酒店业务增强
This commit is contained in:
1 parent
c5eba0423f
commit
f784f75eb3
51 files changed
+3249
-280
No files matched your search
+2
-1
@@ -107,7 +107,8 @@ public interface CanteenDailyMenuService extends IService<CanteenDailyMenuEntity
|
||||
|
||||
/**
|
||||
* 按硬件订单净销量回填当日线上/线下 sold(不改配额)。
|
||||
* <p>口径与 hardware {@code listDailyMenuNetSold} 一致:CANTEEN_DAILY_MENU、
|
||||
* <p>口径与 hardware {@code listDailyMenuNetSold} 一致:
|
||||
* App 每日菜谱 {@code CANTEEN_DAILY_MENU} + 消费机线下 {@code CANTEEN_SPU}/order_type=1;
|
||||
* 已支付/已完成,扣已完成售后;orderType=2→onlineSold,1→offlineSold。
|
||||
* 当日无对应订单的菜谱行 sold 置 0。
|
||||
*
|
||||
|
||||
+8
-1
@@ -1473,6 +1473,10 @@
|
||||
每日菜谱库存剩余:按用餐日 appt_use_date 统计净销量。
|
||||
线上=order_type 2,线下=order_type 1;扣硬售后 status=50 的份数;不含 XFJ。
|
||||
先按父购物车+商品汇总购买量,再扣退款,避免多行购物车重复扣退。
|
||||
商品来源:
|
||||
- App 每日菜谱预约:CANTEEN_DAILY_MENU
|
||||
- 消费机线下购买:CANTEEN_SPU(createCard 默认来源),实时扣减已按当日菜谱分流到 offline_sold
|
||||
若只认 CANTEEN_DAILY_MENU,「按订单重算」会把消费机已售全部刷成 0。
|
||||
-->
|
||||
<select id="listDailyMenuNetSold" resultType="com.bsd.pjcloud.hard.api.vo.order.DailyMenuNetSoldVO">
|
||||
select o.appt_use_date as mealDate,
|
||||
@@ -1511,7 +1515,10 @@
|
||||
and o.hour_type not in ('-1')
|
||||
and o.order_status in ('5', '6', '20')
|
||||
and o.order_type in ('1', '2')
|
||||
and p.product_source = 'CANTEEN_DAILY_MENU'
|
||||
and (
|
||||
p.product_source = 'CANTEEN_DAILY_MENU'
|
||||
or (o.order_type = '1' and p.product_source = 'CANTEEN_SPU')
|
||||
)
|
||||
<if test="dto.hourTypes != null and dto.hourTypes.size() != 0">
|
||||
and o.hour_type in
|
||||
<foreach collection="dto.hourTypes" item="v" open="(" close=")" separator=",">
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.bsd.pjcloud.supermarket.constant;
|
||||
|
||||
/**
|
||||
* 酒店第三方渠道方向:上游给我们数据,下游我们推数据帮我们卖。
|
||||
*/
|
||||
public final class SmHotelChannelDirection {
|
||||
|
||||
/** 上游:供应商 → 本平台 */
|
||||
public static final String UPSTREAM = "upstream";
|
||||
|
||||
/** 下游:本平台 → 销售方 */
|
||||
public static final String DOWNSTREAM = "downstream";
|
||||
|
||||
private SmHotelChannelDirection() {
|
||||
}
|
||||
|
||||
public static String normalize(String direction) {
|
||||
String d = direction == null ? "" : direction.trim().toLowerCase();
|
||||
if (DOWNSTREAM.equals(d)) {
|
||||
return DOWNSTREAM;
|
||||
}
|
||||
return UPSTREAM;
|
||||
}
|
||||
|
||||
public static boolean isDownstream(String direction) {
|
||||
return DOWNSTREAM.equals(normalize(direction));
|
||||
}
|
||||
|
||||
public static boolean isUpstream(String direction) {
|
||||
return !isDownstream(direction);
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.bsd.pjcloud.supermarket.constant;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 下游销售方对接协议(按对方公司/系统隔离,与伙伴编码无关)。
|
||||
* 新下游若接口不同,新增协议常量 + 对应 Adapter 即可。
|
||||
*/
|
||||
public final class SmHotelDownstreamProtocol {
|
||||
|
||||
/** 环启:docs/xiayou 目录下《第三方数据同步接口文档》 */
|
||||
public static final String HUANQI = "huanqi";
|
||||
|
||||
/** @deprecated 历史误用名,normalize 后等价 {@link #HUANQI} */
|
||||
@Deprecated
|
||||
public static final String XIAYOU = "xiayou";
|
||||
|
||||
private SmHotelDownstreamProtocol() {
|
||||
}
|
||||
|
||||
public static String normalize(String protocol) {
|
||||
String p = protocol == null ? "" : protocol.trim().toLowerCase();
|
||||
if (p.isEmpty() || XIAYOU.equals(p)) {
|
||||
return HUANQI;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/** 当前已注册可配置的协议(后台下拉) */
|
||||
public static Set<String> supported() {
|
||||
Set<String> set = new LinkedHashSet<>();
|
||||
set.add(HUANQI);
|
||||
return set;
|
||||
}
|
||||
|
||||
public static boolean isSupported(String protocol) {
|
||||
return supported().contains(normalize(protocol));
|
||||
}
|
||||
|
||||
public static String label(String protocol) {
|
||||
String p = normalize(protocol);
|
||||
if (HUANQI.equals(p)) {
|
||||
return "环启";
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
public static String blankToDefault(String protocol) {
|
||||
return normalize(StrUtil.blankToDefault(StrUtil.trim(protocol), HUANQI));
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.bsd.pjcloud.supermarket.constant;
|
||||
|
||||
/**
|
||||
* 下游推送授权方式。
|
||||
*/
|
||||
public final class SmHotelDownstreamScopeType {
|
||||
|
||||
/** 按部门(+ 可选位置区域) */
|
||||
public static final String DEPT = "dept";
|
||||
|
||||
/** 按指定酒店 */
|
||||
public static final String HOTEL = "hotel";
|
||||
|
||||
private SmHotelDownstreamScopeType() {
|
||||
}
|
||||
|
||||
public static String normalize(String scopeType) {
|
||||
String s = scopeType == null ? "" : scopeType.trim().toLowerCase();
|
||||
if (HOTEL.equals(s)) {
|
||||
return HOTEL;
|
||||
}
|
||||
return DEPT;
|
||||
}
|
||||
|
||||
public static boolean isHotel(String scopeType) {
|
||||
return HOTEL.equals(normalize(scopeType));
|
||||
}
|
||||
|
||||
public static boolean isDept(String scopeType) {
|
||||
return !isHotel(scopeType);
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.bsd.pjcloud.supermarket.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 下游销售方对接参数(YAML/JSON 解析后)。
|
||||
* 环启等下游:本系统作为 A,主动 POST 到 B 的 /third/party/api/*。
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "下游渠道对接配置")
|
||||
public class SmHotelDownstreamChannelConfigDTO {
|
||||
|
||||
@Schema(description = "协议类型,如 huanqi(可与库表 protocol 字段互为补充)")
|
||||
private String protocol;
|
||||
|
||||
@Schema(description = "下游 B 系统根地址,如 http://host/prod-api")
|
||||
private String baseUrl;
|
||||
|
||||
@Schema(description = "B 系统分配的 appKey")
|
||||
private String appKey;
|
||||
|
||||
@Schema(description = "B 系统分配的 appSecret(仅本地签名)")
|
||||
private String appSecret;
|
||||
|
||||
@Schema(description = "HTTP 超时毫秒")
|
||||
private Integer timeoutMs;
|
||||
|
||||
@Schema(description = "推送日历天数(预留)")
|
||||
private Integer dailyDays;
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package com.bsd.pjcloud.supermarket.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 商游(飞猪链路)对接参数:从渠道 config 正文(YAML/JSON)反序列化。
|
||||
* 键名兼容 kebab-case 与 camelCase;未填字段回落 YAML/Nacos {@code shangyou-hotel.*}。
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "商游渠道对接参数")
|
||||
public class SmHotelShangyouChannelConfigDTO {
|
||||
|
||||
@Schema(description = "对方根地址,如 https://xxx/link/provider")
|
||||
private String baseUrl;
|
||||
|
||||
@Schema(description = "鉴权根地址;空则用 baseUrl")
|
||||
private String authBaseUrl;
|
||||
|
||||
@Schema(description = "业务根地址;空则用 baseUrl")
|
||||
private String bizBaseUrl;
|
||||
|
||||
private String clientId;
|
||||
|
||||
private String clientSecret;
|
||||
|
||||
private String cardId;
|
||||
|
||||
private String cardPassword;
|
||||
|
||||
private String callbackSecret;
|
||||
|
||||
private String bookerId;
|
||||
|
||||
private Boolean orderIsGroup;
|
||||
|
||||
private String orderGuaranteeMode;
|
||||
|
||||
private String roomPricePath;
|
||||
|
||||
private String memberLevels;
|
||||
|
||||
private Integer timeoutMs;
|
||||
|
||||
private Integer tokenSkewSeconds;
|
||||
|
||||
private Integer priceDays;
|
||||
|
||||
private Integer hotelListCacheMinutes;
|
||||
|
||||
private Integer hotelListWarnSize;
|
||||
|
||||
private Long callbackTenantId;
|
||||
|
||||
private Boolean callbackQueueEnabled;
|
||||
|
||||
private Integer callbackMaxAttempts;
|
||||
|
||||
private Integer callbackTaskIntervalMs;
|
||||
|
||||
private Double priceApiQps;
|
||||
|
||||
private Integer appPayTimeoutMinutes;
|
||||
|
||||
/** 总开关:false 则本服务 Spring Cron 日更全部跳过(测试环境可关) */
|
||||
private Boolean springScheduleEnabled;
|
||||
|
||||
private Boolean hotelListDailySyncEnabled;
|
||||
|
||||
private String hotelListDailyCron;
|
||||
|
||||
private Boolean priceRollDailyEnabled;
|
||||
|
||||
private String priceRollDailyCron;
|
||||
|
||||
private Boolean localDailyRollEnabled;
|
||||
|
||||
private String localDailyRollCron;
|
||||
|
||||
private Boolean autoImportNewHotels;
|
||||
|
||||
private Integer autoImportBatchSize;
|
||||
|
||||
private Integer priceRollDayOffset;
|
||||
|
||||
private Integer priceRollBatchSize;
|
||||
|
||||
private Integer priceRollIntervalMs;
|
||||
|
||||
private Integer fullPriceSyncMaxPerDay;
|
||||
|
||||
private Integer fullPriceSyncMinIntervalHours;
|
||||
|
||||
private Integer detailPullIntervalMs;
|
||||
|
||||
private Integer detailPullMaxHotels;
|
||||
|
||||
private Integer diffScanBatchSize;
|
||||
|
||||
private Boolean autoMarkOfflineHotels;
|
||||
|
||||
private Boolean autoReactivateOnlineHotels;
|
||||
|
||||
private String defaultImportDataSource;
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package com.bsd.pjcloud.supermarket.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.baomidou.mybatisplus.extension.activerecord.Model;
|
||||
import com.bsd.pjcloud.common.core.util.TenantTable;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TenantTable
|
||||
@TableName("canteen_hotel_channel_config")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "酒店第三方渠道对接配置")
|
||||
public class SmHotelChannelConfigEntity extends Model<SmHotelChannelConfigEntity> {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "渠道标识(上游对齐字典;下游为伙伴编码)")
|
||||
private String channel;
|
||||
|
||||
@Schema(description = "upstream上游 / downstream下游")
|
||||
private String direction;
|
||||
|
||||
@Schema(description = "对接协议(下游必填,如 huanqi=环启;上游可空)")
|
||||
private String protocol;
|
||||
|
||||
@Schema(description = "1启用 0停用")
|
||||
private String enabled;
|
||||
|
||||
@Schema(description = "对接参数正文(YAML 或 JSON,可含注释)")
|
||||
private String configJson;
|
||||
|
||||
private String remark;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private String updateBy;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@TableLogic
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String delFlag;
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package com.bsd.pjcloud.supermarket.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.baomidou.mybatisplus.extension.activerecord.Model;
|
||||
import com.bsd.pjcloud.common.core.util.TenantTable;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TenantTable
|
||||
@TableName("canteen_hotel_channel_dept")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "酒店第三方渠道部门映射")
|
||||
public class SmHotelChannelDeptEntity extends Model<SmHotelChannelDeptEntity> {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "渠道/下游伙伴标识(上游对齐字典;下游为伙伴编码)")
|
||||
private String channel;
|
||||
|
||||
@Schema(description = "upstream上游 / downstream下游")
|
||||
private String direction;
|
||||
|
||||
@Schema(description = "下游授权方式:dept按部门 / hotel按指定酒店(上游固定 dept)")
|
||||
private String scopeType;
|
||||
|
||||
@Schema(description = "部门 ID(上游=归属部门;下游 dept 模式=允许推送的部门)")
|
||||
private Long deptId;
|
||||
|
||||
@Schema(description = "位置区域 adcode(下游 dept 模式可选;空=该部门全部区域)")
|
||||
private Long districtAdcode;
|
||||
|
||||
@Schema(description = "位置区域名称(展示/名称兜底)")
|
||||
private String district;
|
||||
|
||||
@Schema(description = "指定酒店 ID,逗号分隔(下游 hotel 模式)")
|
||||
private String hotelIds;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "1启用 0停用")
|
||||
private String status;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private String updateBy;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@TableLogic
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String delFlag;
|
||||
|
||||
/** 列表展示:部门名称(非表字段) */
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "部门名称")
|
||||
private String deptName;
|
||||
|
||||
/** 列表展示:渠道字典标签(非表字段) */
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "渠道名称")
|
||||
private String channelLabel;
|
||||
|
||||
/** 列表展示:指定酒店摘要(非表字段) */
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "指定酒店摘要")
|
||||
private String hotelNames;
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package com.bsd.pjcloud.supermarket.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.baomidou.mybatisplus.extension.activerecord.Model;
|
||||
import com.bsd.pjcloud.common.core.util.TenantTable;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TenantTable
|
||||
@TableName("canteen_hotel_downstream_push_log")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "酒店下游推送流水")
|
||||
public class SmHotelDownstreamPushLogEntity extends Model<SmHotelDownstreamPushLogEntity> {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "下游伙伴标识")
|
||||
private String partnerCode;
|
||||
|
||||
@Schema(description = "本系统酒店ID")
|
||||
private Long hotelId;
|
||||
|
||||
@Schema(description = "外部酒店ID")
|
||||
private String extHotelId;
|
||||
|
||||
@Schema(description = "动作:hotel_sync/apartment_sync/product_sync/daily_sync")
|
||||
private String action;
|
||||
|
||||
@Schema(description = "完整请求URL")
|
||||
private String requestUrl;
|
||||
|
||||
@Schema(description = "请求体原文")
|
||||
private String requestBody;
|
||||
|
||||
@Schema(description = "响应体原文")
|
||||
private String responseBody;
|
||||
|
||||
@Schema(description = "HTTP状态码")
|
||||
private Integer httpStatus;
|
||||
|
||||
@Schema(description = "业务code")
|
||||
private Integer bizCode;
|
||||
|
||||
@Schema(description = "1成功 0失败")
|
||||
private String success;
|
||||
|
||||
@Schema(description = "失败摘要")
|
||||
private String errorMsg;
|
||||
|
||||
@Schema(description = "耗时毫秒")
|
||||
private Long costMs;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private String updateBy;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@TableLogic
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String delFlag;
|
||||
}
|
||||
+1
-1
@@ -124,7 +124,7 @@ public class SmHotelOrderEntity extends Model<SmHotelOrderEntity> {
|
||||
@Schema(description = "下单来源")
|
||||
private String orderSource;
|
||||
|
||||
/** 列表展示:酒店数据来源 local/shangyou(非表字段) */
|
||||
/** 列表展示:酒店数据来源 local/shangyou/其它渠道(非表字段) */
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "酒店数据来源")
|
||||
private String hotelDataSource;
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package com.bsd.pjcloud.supermarket.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.baomidou.mybatisplus.extension.activerecord.Model;
|
||||
import com.bsd.pjcloud.common.core.util.TenantTable;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TenantTable
|
||||
@TableName("canteen_hotel_order_upstream_log")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "酒店订单上游调用流水")
|
||||
public class SmHotelOrderUpstreamLogEntity extends Model<SmHotelOrderUpstreamLogEntity> {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "订单 ID")
|
||||
private Long orderId;
|
||||
|
||||
@Schema(description = "渠道 local/shangyou/...")
|
||||
private String channel;
|
||||
|
||||
@Schema(description = "CREATE/QUERY/CANCEL/RETRY_ADD/AUTO_REFUND/CHECK_AVAIL")
|
||||
private String action;
|
||||
|
||||
@Schema(description = "1成功 0失败")
|
||||
private String success;
|
||||
|
||||
private String bizCode;
|
||||
|
||||
private String message;
|
||||
|
||||
private String reqJson;
|
||||
|
||||
private String respJson;
|
||||
|
||||
private Long costMs;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private String updateBy;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@TableLogic
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String delFlag;
|
||||
}
|
||||
+5
-1
@@ -2,6 +2,7 @@ package com.bsd.pjcloud.supermarket.vo;
|
||||
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelDailyEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelOrderEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelOrderUpstreamLogEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelProductEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
@@ -40,6 +41,9 @@ public class SmHotelOrderDetailVo {
|
||||
@Schema(description = "上游响应快照(解析自 upstreamRespJson)")
|
||||
private Object upstreamResp;
|
||||
|
||||
@Schema(description = "上游调用流水(CREATE/QUERY/CANCEL/AUTO_REFUND 等)")
|
||||
private List<SmHotelOrderUpstreamLogEntity> upstreamLogs;
|
||||
|
||||
@Data
|
||||
@Schema(description = "酒店摘要")
|
||||
public static class HotelBrief {
|
||||
@@ -52,7 +56,7 @@ public class SmHotelOrderDetailVo {
|
||||
|
||||
private String groupCode;
|
||||
|
||||
/** local / shangyou */
|
||||
/** 数据来源:local / shangyou / 其它渠道 sheet_name */
|
||||
private String dataSource;
|
||||
|
||||
private String upstreamStatus;
|
||||
|
||||
+7
@@ -6,6 +6,7 @@ import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 下游销售方推送配置(本系统作为 A,主动推送到 B 的 /third/party/api/*)。
|
||||
* 优先读库表「渠道对接配置」direction=downstream 的 YAML;本类作 YAML/Nacos 单下游兼容回落。
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@@ -15,6 +16,12 @@ public class HotelDownstreamProperties {
|
||||
/** 是否启用向下游推送 */
|
||||
private boolean enabled = false;
|
||||
|
||||
/**
|
||||
* 对接协议:huanqi(环启)等,决定走哪个 Adapter。
|
||||
* @see com.bsd.pjcloud.supermarket.constant.SmHotelDownstreamProtocol
|
||||
*/
|
||||
private String protocol = "huanqi";
|
||||
|
||||
/** 下游 B 系统根地址,例 https://b.example.com ,实际请求 = baseUrl + /third/party/api/hotel/sync */
|
||||
private String baseUrl = "";
|
||||
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package com.bsd.pjcloud.supermarket.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.log.annotation.SysLog;
|
||||
import com.bsd.pjcloud.supermarket.config.HotelDownstreamProperties;
|
||||
import com.bsd.pjcloud.supermarket.config.ShangyouHotelProperties;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelChannelConfigEntity;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelConfigService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/smHotelChannelConfig")
|
||||
@Tag(name = "酒店第三方渠道对接配置(YAML/JSON 热更新,区分上下游)")
|
||||
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
|
||||
public class SmHotelChannelConfigController {
|
||||
|
||||
private final SmHotelChannelConfigService channelConfigService;
|
||||
|
||||
@Operation(summary = "分页")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelChannelConfig_view')")
|
||||
public R<Page<SmHotelChannelConfigEntity>> page(@ParameterObject Page<SmHotelChannelConfigEntity> page,
|
||||
@RequestParam(required = false) String channel,
|
||||
@RequestParam(required = false) String direction) {
|
||||
return R.ok(channelConfigService.pageConfigs(page, channel, direction));
|
||||
}
|
||||
|
||||
@Operation(summary = "按渠道+方向查询")
|
||||
@GetMapping("/byChannel")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelChannelConfig_view')")
|
||||
public R<SmHotelChannelConfigEntity> byChannel(@RequestParam String channel,
|
||||
@RequestParam(required = false) String direction) {
|
||||
return R.ok(channelConfigService.getByChannel(channel, direction));
|
||||
}
|
||||
|
||||
@Operation(summary = "上游 YAML 默认值(表单占位对照)")
|
||||
@GetMapping("/yamlDefaults")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelChannelConfig_view')")
|
||||
public R<ShangyouHotelProperties> yamlDefaults() {
|
||||
return R.ok(channelConfigService.yamlDefaultsSnapshot());
|
||||
}
|
||||
|
||||
@Operation(summary = "下游 YAML 默认值(表单占位对照)")
|
||||
@GetMapping("/downstreamYamlDefaults")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelChannelConfig_view')")
|
||||
public R<HotelDownstreamProperties> downstreamYamlDefaults() {
|
||||
return R.ok(channelConfigService.downstreamYamlDefaultsSnapshot());
|
||||
}
|
||||
|
||||
@Operation(summary = "已实现的下游协议列表")
|
||||
@GetMapping("/downstreamProtocols")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelChannelConfig_view')")
|
||||
public R<java.util.List<String>> downstreamProtocols() {
|
||||
return R.ok(channelConfigService.listDownstreamProtocols());
|
||||
}
|
||||
|
||||
@Operation(summary = "已配置的下游伙伴列表(范围映射下拉)")
|
||||
@GetMapping("/downstreamPartners")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelChannelConfig_view')")
|
||||
public R<java.util.List<SmHotelChannelConfigEntity>> downstreamPartners() {
|
||||
return R.ok(channelConfigService.listDownstreamPartners());
|
||||
}
|
||||
|
||||
@Operation(summary = "当前生效的商游运行时配置(库表覆盖后)")
|
||||
@GetMapping("/shangyouRuntime")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelChannelConfig_view')")
|
||||
public R<ShangyouHotelProperties> shangyouRuntime() {
|
||||
return R.ok(channelConfigService.resolveShangyouRuntime());
|
||||
}
|
||||
|
||||
@SysLog("保存酒店渠道对接配置")
|
||||
@PostMapping
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelChannelConfig_edit')")
|
||||
public R<Boolean> save(@RequestBody SmHotelChannelConfigEntity entity) {
|
||||
return channelConfigService.saveOrUpdateByChannel(entity);
|
||||
}
|
||||
|
||||
@SysLog("修改酒店渠道对接配置")
|
||||
@PutMapping
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelChannelConfig_edit')")
|
||||
public R<Boolean> update(@RequestBody SmHotelChannelConfigEntity entity) {
|
||||
return channelConfigService.saveOrUpdateByChannel(entity);
|
||||
}
|
||||
|
||||
@SysLog("删除酒店渠道对接配置")
|
||||
@DeleteMapping
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelChannelConfig_edit')")
|
||||
public R<Boolean> remove(@RequestBody Long[] ids) {
|
||||
return channelConfigService.removeByIdsSafe(ids);
|
||||
}
|
||||
|
||||
@SysLog("刷新渠道对接配置缓存")
|
||||
@PostMapping("/evictCache")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelChannelConfig_edit')")
|
||||
public R<Boolean> evictCache(@RequestParam(required = false) String channel) {
|
||||
channelConfigService.evictCache(channel);
|
||||
return R.ok(Boolean.TRUE);
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package com.bsd.pjcloud.supermarket.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.log.annotation.SysLog;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelChannelDeptEntity;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelDeptService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/smHotelChannelDept")
|
||||
@Tag(name = "酒店第三方渠道部门映射")
|
||||
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
|
||||
public class SmHotelChannelDeptController {
|
||||
|
||||
private final SmHotelChannelDeptService channelDeptService;
|
||||
|
||||
@Operation(summary = "分页")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelChannelDept_view')")
|
||||
public R<Page<SmHotelChannelDeptEntity>> page(@ParameterObject Page<SmHotelChannelDeptEntity> page,
|
||||
@ParameterObject SmHotelChannelDeptEntity query) {
|
||||
return R.ok(channelDeptService.pageMappings(page, query));
|
||||
}
|
||||
|
||||
@Operation(summary = "启用中的渠道部门(同步/拉取下拉)")
|
||||
@GetMapping("/listEnabled")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync') || @pms.hasPermission('supermarket_smHotelChannelDept_view') || @pms.hasPermission('supermarket_smHotel_add')")
|
||||
public R<List<SmHotelChannelDeptEntity>> listEnabled() {
|
||||
return R.ok(channelDeptService.listEnabled());
|
||||
}
|
||||
|
||||
@SysLog("新增酒店渠道部门映射")
|
||||
@PostMapping
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelChannelDept_add')")
|
||||
public R<Boolean> save(@RequestBody SmHotelChannelDeptEntity entity) {
|
||||
return channelDeptService.saveMapping(entity);
|
||||
}
|
||||
|
||||
@SysLog("修改酒店渠道部门映射")
|
||||
@PutMapping
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelChannelDept_edit')")
|
||||
public R<Boolean> update(@RequestBody SmHotelChannelDeptEntity entity) {
|
||||
return channelDeptService.updateMapping(entity);
|
||||
}
|
||||
|
||||
@SysLog("删除酒店渠道部门映射")
|
||||
@DeleteMapping
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelChannelDept_del')")
|
||||
public R<Boolean> remove(@RequestBody Long[] ids) {
|
||||
return channelDeptService.removeMappings(ids);
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.bsd.pjcloud.supermarket.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelDownstreamPushLogEntity;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelDownstreamPushLogService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/smHotelDownstreamPushLog")
|
||||
@Tag(name = "酒店下游推送流水")
|
||||
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
|
||||
public class SmHotelDownstreamPushLogController {
|
||||
|
||||
private final SmHotelDownstreamPushLogService pushLogService;
|
||||
|
||||
@Operation(summary = "分页")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelDownstreamPushLog_view')")
|
||||
public R<Page<SmHotelDownstreamPushLogEntity>> page(@ParameterObject Page<SmHotelDownstreamPushLogEntity> page,
|
||||
@ParameterObject SmHotelDownstreamPushLogEntity query) {
|
||||
return R.ok(pushLogService.pageLogs(page, query));
|
||||
}
|
||||
|
||||
@Operation(summary = "详情")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelDownstreamPushLog_view')")
|
||||
public R<SmHotelDownstreamPushLogEntity> detail(@PathVariable Long id) {
|
||||
return R.ok(pushLogService.getById(id));
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -298,12 +298,13 @@ public class SmHotelMidController {
|
||||
}
|
||||
|
||||
@SysLog("全量拉取启用酒店详情")
|
||||
@Operation(summary = "异步拉取全部启用酒店详情(房型+产品+未来房价),立刻返回 logId")
|
||||
@Operation(summary = "异步拉取指定渠道启用酒店详情(房型+产品+未来房价),立刻返回 logId")
|
||||
@PostMapping("/mid/pullAllActiveDetails")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
|
||||
public R<Map<String, Object>> pullAllActiveDetails(
|
||||
@RequestParam(required = false) Integer limit) {
|
||||
R<SmHotelSyncLogEntity> started = syncService.startDetailPullAsync(limit, "MANUAL");
|
||||
@RequestParam(required = false) Integer limit,
|
||||
@RequestParam("dataSource") String dataSource) {
|
||||
R<SmHotelSyncLogEntity> started = syncService.startDetailPullAsync(limit, "MANUAL", dataSource);
|
||||
if (started.getCode() != 0) {
|
||||
return R.failed(started.getMsg());
|
||||
}
|
||||
@@ -311,6 +312,7 @@ public class SmHotelMidController {
|
||||
data.put("async", true);
|
||||
data.put("logId", started.getData().getId());
|
||||
data.put("status", started.getData().getStatus());
|
||||
data.put("dataSource", dataSource);
|
||||
data.put("message", "已提交,请轮询 /mid/syncLog/{id} 或打开同步日志");
|
||||
return R.ok(data, started.getMsg());
|
||||
}
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.bsd.pjcloud.supermarket.event;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 上游硬失败后、事务提交再触发原路退款(避免嵌在支付回调长事务内)。
|
||||
*/
|
||||
@Getter
|
||||
public class HotelUpstreamFailRefundEvent {
|
||||
|
||||
private final Long orderId;
|
||||
|
||||
private final Long tenantId;
|
||||
|
||||
private final String reason;
|
||||
|
||||
public HotelUpstreamFailRefundEvent(Long orderId, Long tenantId, String reason) {
|
||||
this.orderId = orderId;
|
||||
this.tenantId = tenantId;
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package com.bsd.pjcloud.supermarket.event;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.data.tenant.TenantBroker;
|
||||
import com.bsd.pjcloud.common.data.tenant.TenantContextHolder;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelOrderEntity;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelOrderMapper;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelOrderService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelOrderUpstreamLogService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.event.TransactionPhase;
|
||||
import org.springframework.transaction.event.TransactionalEventListener;
|
||||
|
||||
/**
|
||||
* 支付后上游硬失败:提交后自动原路退款,并写 AUTO_REFUND 流水。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class HotelUpstreamFailRefundListener {
|
||||
|
||||
private final SmHotelOrderService orderService;
|
||||
|
||||
private final SmHotelOrderMapper orderMapper;
|
||||
|
||||
private final SmHotelOrderUpstreamLogService upstreamLogService;
|
||||
|
||||
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
|
||||
public void onUpstreamFail(HotelUpstreamFailRefundEvent event) {
|
||||
if (event == null || event.getOrderId() == null) {
|
||||
return;
|
||||
}
|
||||
Long tenantId = event.getTenantId() != null ? event.getTenantId() : TenantContextHolder.getTenantId();
|
||||
if (tenantId == null) {
|
||||
tenantId = 1L;
|
||||
}
|
||||
Long tid = tenantId;
|
||||
try {
|
||||
TenantBroker.runAs(tid, id -> doRefund(event));
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.error("|酒店自动退款|异常|orderId={}|", event.getOrderId(), e);
|
||||
markRefundNeedManual(event.getOrderId(), "自动退款异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void doRefund(HotelUpstreamFailRefundEvent event) {
|
||||
String reason = StrUtil.blankToDefault(event.getReason(), "上游下单失败自动退款");
|
||||
long t0 = System.currentTimeMillis();
|
||||
R<Boolean> res = orderService.refundByAdmin(event.getOrderId(), reason);
|
||||
long cost = System.currentTimeMillis() - t0;
|
||||
boolean ok = res != null && res.isOk();
|
||||
upstreamLogService.saveSimple(event.getOrderId(), "auto", "AUTO_REFUND", ok,
|
||||
ok ? "OK" : "FAIL", ok ? "自动退款已受理" : (res == null ? "退款无响应" : res.getMsg()),
|
||||
null, null, cost);
|
||||
if (!ok) {
|
||||
markRefundNeedManual(event.getOrderId(),
|
||||
"上游失败且自动退款失败: " + (res == null ? "无响应" : StrUtil.blankToDefault(res.getMsg(), "未知")));
|
||||
}
|
||||
log.info("|酒店自动退款|完成|orderId={}|ok={}|msg={}|", event.getOrderId(), ok,
|
||||
res == null ? null : res.getMsg());
|
||||
}
|
||||
|
||||
private void markRefundNeedManual(Long orderId, String msg) {
|
||||
SmHotelOrderEntity order = orderMapper.selectById(orderId);
|
||||
if (order == null) {
|
||||
return;
|
||||
}
|
||||
order.setCompensateStatus("NEED_MANUAL");
|
||||
order.setFailMsg(StrUtil.sub(StrUtil.blankToDefault(order.getFailMsg(), "") + ";" + msg, 0, 500));
|
||||
order.setLastCompensateMsg(StrUtil.sub(msg, 0, 500));
|
||||
orderMapper.updateById(order);
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.bsd.pjcloud.supermarket.integration.downstream;
|
||||
|
||||
import cn.hutool.json.JSONObject;
|
||||
import com.bsd.pjcloud.supermarket.config.HotelDownstreamProperties;
|
||||
|
||||
/**
|
||||
* 下游酒店推送协议适配器。一种协议一份实现,互不影响。
|
||||
*/
|
||||
public interface DownstreamHotelAdapter {
|
||||
|
||||
/** 协议标识,对齐 {@link com.bsd.pjcloud.supermarket.constant.SmHotelDownstreamProtocol} */
|
||||
String protocol();
|
||||
|
||||
void assertReady(String partnerCode, HotelDownstreamProperties props);
|
||||
|
||||
JSONObject hotelSync(String partnerCode, HotelDownstreamProperties props, Long hotelId, String extHotelId,
|
||||
Object body);
|
||||
|
||||
JSONObject apartmentSync(String partnerCode, HotelDownstreamProperties props, Long hotelId, String extHotelId,
|
||||
Object body);
|
||||
|
||||
JSONObject productSync(String partnerCode, HotelDownstreamProperties props, Long hotelId, String extHotelId,
|
||||
Object body);
|
||||
|
||||
JSONObject dailySync(String partnerCode, HotelDownstreamProperties props, Long hotelId, String extHotelId,
|
||||
Object body);
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package com.bsd.pjcloud.supermarket.integration.downstream;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.bsd.pjcloud.common.core.exception.CheckedException;
|
||||
import com.bsd.pjcloud.supermarket.constant.SmHotelDownstreamProtocol;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 按协议路由下游适配器;新协议只需新增 Adapter Bean。
|
||||
*/
|
||||
@Component
|
||||
public class DownstreamHotelAdapterRegistry {
|
||||
|
||||
private final Map<String, DownstreamHotelAdapter> byProtocol = new LinkedHashMap<>();
|
||||
|
||||
public DownstreamHotelAdapterRegistry(List<DownstreamHotelAdapter> adapters) {
|
||||
if (adapters != null) {
|
||||
for (DownstreamHotelAdapter adapter : adapters) {
|
||||
if (adapter == null || StrUtil.isBlank(adapter.protocol())) {
|
||||
continue;
|
||||
}
|
||||
String key = SmHotelDownstreamProtocol.normalize(adapter.protocol());
|
||||
byProtocol.put(key, adapter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DownstreamHotelAdapter require(String protocol) {
|
||||
String key = SmHotelDownstreamProtocol.normalize(protocol);
|
||||
DownstreamHotelAdapter adapter = byProtocol.get(key);
|
||||
if (adapter == null) {
|
||||
throw new CheckedException("下游协议「" + key + "」尚未实现适配器,已注册:"
|
||||
+ String.join(",", byProtocol.keySet()));
|
||||
}
|
||||
return adapter;
|
||||
}
|
||||
|
||||
public Collection<String> registeredProtocols() {
|
||||
return byProtocol.keySet();
|
||||
}
|
||||
}
|
||||
+26
-68
@@ -1,97 +1,55 @@
|
||||
package com.bsd.pjcloud.supermarket.integration.downstream;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.bsd.pjcloud.supermarket.config.HotelDownstreamProperties;
|
||||
import com.bsd.pjcloud.common.core.exception.CheckedException;
|
||||
import com.bsd.pjcloud.supermarket.constant.SmHotelDownstreamProtocol;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelConfigService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 向下游销售方(B系统)推送酒店/房型/产品/日历房。
|
||||
* 下游推送门面:按伙伴配置的 protocol 路由到对应适配器。
|
||||
* 业务层只认伙伴编码,不关心对方系统差异。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DownstreamHotelClient {
|
||||
|
||||
private static final String TAG = "下游酒店推送";
|
||||
private final SmHotelChannelConfigService channelConfigService;
|
||||
|
||||
private final HotelDownstreamProperties properties;
|
||||
private final DownstreamHotelAdapterRegistry adapterRegistry;
|
||||
|
||||
public void assertEnabled() {
|
||||
if (!properties.isEnabled()) {
|
||||
throw new CheckedException("下游推送未启用,请配置 hotel-downstream.enabled=true");
|
||||
}
|
||||
if (StrUtil.isBlank(properties.getBaseUrl()) || StrUtil.isBlank(properties.getAppKey())
|
||||
|| StrUtil.isBlank(properties.getAppSecret())) {
|
||||
throw new CheckedException("下游推送配置不完整:base-url / app-key / app-secret");
|
||||
}
|
||||
public void assertPartnerReady(String partnerCode) {
|
||||
HotelDownstreamProperties props = resolve(partnerCode);
|
||||
adapter(props).assertReady(partnerCode, props);
|
||||
}
|
||||
|
||||
public JSONObject hotelSync(Object body) {
|
||||
return post(DownstreamHotelApi.HOTEL_SYNC, body);
|
||||
public HotelDownstreamProperties resolve(String partnerCode) {
|
||||
return channelConfigService.resolveDownstreamRuntime(partnerCode);
|
||||
}
|
||||
|
||||
public JSONObject apartmentSync(Object body) {
|
||||
return post(DownstreamHotelApi.APARTMENT_SYNC, body);
|
||||
public JSONObject hotelSync(String partnerCode, Long hotelId, String extHotelId, Object body) {
|
||||
HotelDownstreamProperties props = resolve(partnerCode);
|
||||
return adapter(props).hotelSync(partnerCode, props, hotelId, extHotelId, body);
|
||||
}
|
||||
|
||||
public JSONObject productSync(Object body) {
|
||||
return post(DownstreamHotelApi.PRODUCT_SYNC, body);
|
||||
public JSONObject apartmentSync(String partnerCode, Long hotelId, String extHotelId, Object body) {
|
||||
HotelDownstreamProperties props = resolve(partnerCode);
|
||||
return adapter(props).apartmentSync(partnerCode, props, hotelId, extHotelId, body);
|
||||
}
|
||||
|
||||
public JSONObject dailySync(Object body) {
|
||||
return post(DownstreamHotelApi.DAILY_SYNC, body);
|
||||
public JSONObject productSync(String partnerCode, Long hotelId, String extHotelId, Object body) {
|
||||
HotelDownstreamProperties props = resolve(partnerCode);
|
||||
return adapter(props).productSync(partnerCode, props, hotelId, extHotelId, body);
|
||||
}
|
||||
|
||||
private JSONObject post(String path, Object bodyObj) {
|
||||
assertEnabled();
|
||||
String bodyJson = bodyObj instanceof String ? (String) bodyObj : JSONUtil.toJsonStr(bodyObj);
|
||||
String timestamp = String.valueOf(System.currentTimeMillis());
|
||||
String nonce = IdUtil.fastSimpleUUID();
|
||||
String sign = DownstreamSignHelper.sign(properties.getAppKey(), properties.getAppSecret(), timestamp, nonce,
|
||||
bodyJson);
|
||||
String url = trimSlash(properties.getBaseUrl()) + path;
|
||||
log.info("|{}|req|path={}|body={}|", TAG, path, bodyJson);
|
||||
try (HttpResponse response = HttpRequest.post(url)
|
||||
.timeout(properties.getTimeoutMs())
|
||||
.header("Content-Type", "application/json;charset=UTF-8")
|
||||
.header("appKey", properties.getAppKey())
|
||||
.header("timestamp", timestamp)
|
||||
.header("nonce", nonce)
|
||||
.header("sign", sign)
|
||||
.body(bodyJson)
|
||||
.execute()) {
|
||||
String res = response.body();
|
||||
log.info("|{}|res|path={}|http={}|body={}|", TAG, path, response.getStatus(), res);
|
||||
if (StrUtil.isBlank(res)) {
|
||||
throw new CheckedException("下游响应为空: " + path);
|
||||
}
|
||||
JSONObject json = JSONUtil.parseObj(res);
|
||||
Integer code = json.getInt("code");
|
||||
if (code == null || code != 200) {
|
||||
throw new CheckedException(StrUtil.format("下游失败[{}] code={} msg={}", path, code, json.getStr("msg")));
|
||||
}
|
||||
return json;
|
||||
}
|
||||
catch (CheckedException e) {
|
||||
throw e;
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new CheckedException("下游调用失败: " + e.getMessage());
|
||||
}
|
||||
public JSONObject dailySync(String partnerCode, Long hotelId, String extHotelId, Object body) {
|
||||
HotelDownstreamProperties props = resolve(partnerCode);
|
||||
return adapter(props).dailySync(partnerCode, props, hotelId, extHotelId, body);
|
||||
}
|
||||
|
||||
private static String trimSlash(String base) {
|
||||
if (base == null) {
|
||||
return "";
|
||||
}
|
||||
return base.endsWith("/") ? base.substring(0, base.length() - 1) : base;
|
||||
private DownstreamHotelAdapter adapter(HotelDownstreamProperties props) {
|
||||
String protocol = props == null ? SmHotelDownstreamProtocol.HUANQI : props.getProtocol();
|
||||
return adapterRegistry.require(protocol);
|
||||
}
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
package com.bsd.pjcloud.supermarket.integration.downstream.huanqi;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.bsd.pjcloud.common.core.exception.CheckedException;
|
||||
import com.bsd.pjcloud.supermarket.config.HotelDownstreamProperties;
|
||||
import com.bsd.pjcloud.supermarket.constant.SmHotelDownstreamProtocol;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelDownstreamPushLogEntity;
|
||||
import com.bsd.pjcloud.supermarket.integration.downstream.DownstreamHotelAdapter;
|
||||
import com.bsd.pjcloud.supermarket.integration.downstream.DownstreamHotelApi;
|
||||
import com.bsd.pjcloud.supermarket.integration.downstream.DownstreamSignHelper;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelDownstreamPushLogService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 环启下游协议:本系统作为 A,POST B 的 /third/party/api/* + MD5 签名。
|
||||
* 文档见 docs/xiayou《第三方数据同步接口文档》。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class HuanqiDownstreamHotelAdapter implements DownstreamHotelAdapter {
|
||||
|
||||
private static final String TAG = "下游推送[环启]";
|
||||
|
||||
private final SmHotelDownstreamPushLogService pushLogService;
|
||||
|
||||
@Override
|
||||
public String protocol() {
|
||||
return SmHotelDownstreamProtocol.HUANQI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertReady(String partnerCode, HotelDownstreamProperties props) {
|
||||
if (props == null || !props.isEnabled()) {
|
||||
throw new CheckedException("下游伙伴未启用: " + partnerCode);
|
||||
}
|
||||
if (StrUtil.isBlank(props.getBaseUrl()) || StrUtil.isBlank(props.getAppKey())
|
||||
|| StrUtil.isBlank(props.getAppSecret())) {
|
||||
throw new CheckedException("下游伙伴配置不完整(base-url / app-key / app-secret): " + partnerCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject hotelSync(String partnerCode, HotelDownstreamProperties props, Long hotelId, String extHotelId,
|
||||
Object body) {
|
||||
return post(partnerCode, props, hotelId, extHotelId, "hotel_sync", DownstreamHotelApi.HOTEL_SYNC, body);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject apartmentSync(String partnerCode, HotelDownstreamProperties props, Long hotelId, String extHotelId,
|
||||
Object body) {
|
||||
return post(partnerCode, props, hotelId, extHotelId, "apartment_sync", DownstreamHotelApi.APARTMENT_SYNC, body);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject productSync(String partnerCode, HotelDownstreamProperties props, Long hotelId, String extHotelId,
|
||||
Object body) {
|
||||
return post(partnerCode, props, hotelId, extHotelId, "product_sync", DownstreamHotelApi.PRODUCT_SYNC, body);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject dailySync(String partnerCode, HotelDownstreamProperties props, Long hotelId, String extHotelId,
|
||||
Object body) {
|
||||
return post(partnerCode, props, hotelId, extHotelId, "daily_sync", DownstreamHotelApi.DAILY_SYNC, body);
|
||||
}
|
||||
|
||||
private JSONObject post(String partnerCode, HotelDownstreamProperties props, Long hotelId, String extHotelId,
|
||||
String action, String path, Object bodyObj) {
|
||||
assertReady(partnerCode, props);
|
||||
String bodyJson = bodyObj instanceof String ? (String) bodyObj : JSONUtil.toJsonStr(bodyObj);
|
||||
String timestamp = String.valueOf(System.currentTimeMillis());
|
||||
String nonce = IdUtil.fastSimpleUUID();
|
||||
String sign = DownstreamSignHelper.sign(props.getAppKey(), props.getAppSecret(), timestamp, nonce, bodyJson);
|
||||
String url = trimSlash(props.getBaseUrl()) + path;
|
||||
log.info("|{}|req|partner={}|path={}|body={}|", TAG, partnerCode, path, bodyJson);
|
||||
long start = System.currentTimeMillis();
|
||||
Integer httpStatus = null;
|
||||
String res = null;
|
||||
Integer bizCode = null;
|
||||
boolean ok = false;
|
||||
String errMsg = null;
|
||||
try (HttpResponse response = HttpRequest.post(url)
|
||||
.timeout(props.getTimeoutMs())
|
||||
.header("Content-Type", "application/json;charset=UTF-8")
|
||||
.header("appKey", props.getAppKey())
|
||||
.header("timestamp", timestamp)
|
||||
.header("nonce", nonce)
|
||||
.header("sign", sign)
|
||||
.body(bodyJson)
|
||||
.execute()) {
|
||||
httpStatus = response.getStatus();
|
||||
res = response.body();
|
||||
log.info("|{}|res|partner={}|path={}|http={}|body={}|", TAG, partnerCode, path, httpStatus, res);
|
||||
if (StrUtil.isBlank(res)) {
|
||||
errMsg = "下游响应为空: " + path;
|
||||
throw new CheckedException(errMsg);
|
||||
}
|
||||
JSONObject json = JSONUtil.parseObj(res);
|
||||
bizCode = json.getInt("code");
|
||||
if (bizCode == null || bizCode != 200) {
|
||||
errMsg = StrUtil.format("下游失败[{}] code={} msg={}", path, bizCode, json.getStr("msg"));
|
||||
throw new CheckedException(errMsg);
|
||||
}
|
||||
ok = true;
|
||||
return json;
|
||||
}
|
||||
catch (CheckedException e) {
|
||||
if (errMsg == null) {
|
||||
errMsg = e.getMessage();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
catch (Exception e) {
|
||||
errMsg = "下游调用失败: " + e.getMessage();
|
||||
throw new CheckedException(errMsg);
|
||||
}
|
||||
finally {
|
||||
writeLog(partnerCode, hotelId, extHotelId, action, url, bodyJson, res, httpStatus, bizCode, ok, errMsg,
|
||||
System.currentTimeMillis() - start);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeLog(String partnerCode, Long hotelId, String extHotelId, String action, String url,
|
||||
String requestBody, String responseBody, Integer httpStatus, Integer bizCode, boolean ok, String errMsg,
|
||||
long costMs) {
|
||||
SmHotelDownstreamPushLogEntity row = new SmHotelDownstreamPushLogEntity();
|
||||
row.setPartnerCode(partnerCode);
|
||||
row.setHotelId(hotelId);
|
||||
row.setExtHotelId(extHotelId);
|
||||
row.setAction(action);
|
||||
row.setRequestUrl(url);
|
||||
row.setRequestBody(requestBody);
|
||||
row.setResponseBody(responseBody);
|
||||
row.setHttpStatus(httpStatus);
|
||||
row.setBizCode(bizCode);
|
||||
row.setSuccess(ok ? "1" : "0");
|
||||
row.setErrorMsg(StrUtil.maxLength(errMsg, 900));
|
||||
row.setCostMs(costMs);
|
||||
pushLogService.record(row);
|
||||
}
|
||||
|
||||
private static String trimSlash(String base) {
|
||||
if (base == null) {
|
||||
return "";
|
||||
}
|
||||
return base.endsWith("/") ? base.substring(0, base.length() - 1) : base;
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.bsd.pjcloud.supermarket.integration.hotel.upstream;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
|
||||
/**
|
||||
* 酒店上游订单网关(按数据来源渠道插拔)。
|
||||
* <p>
|
||||
* 新增第三方:实现本接口并注册为 Spring Bean,字典 {@code sm_hotel_data_source} 增加对应 item_value 即可。
|
||||
*/
|
||||
public interface HotelUpstreamOrderGateway {
|
||||
|
||||
/** 渠道标识,与 hotel.sheet_name / 字典值一致,如 shangyou */
|
||||
String channel();
|
||||
|
||||
/** 可定校验,返回上游 content(渠道自定义结构) */
|
||||
JSONObject checkAvailability(JSONObject body);
|
||||
|
||||
/** 创建上游订单 */
|
||||
HotelUpstreamOrderResult createOrder(JSONObject body);
|
||||
|
||||
/** 按下游 requestNo / channelNo 查询 */
|
||||
HotelUpstreamOrderResult queryByRequestNo(String requestNo);
|
||||
|
||||
/** 按上游平台单号查询 */
|
||||
HotelUpstreamOrderResult queryOrder(String platformNo, String hotelCode);
|
||||
|
||||
/** 取消上游订单 */
|
||||
HotelUpstreamOrderResult cancelOrder(String platformNo, String hotelCode);
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
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>已注册渠道:返回对应 Gateway</li>
|
||||
* <li>未注册渠道:{@link #isUnsupportedChannel} 为 true,履约应硬失败</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Component
|
||||
public class HotelUpstreamOrderGatewayRegistry {
|
||||
|
||||
private final Map<String, HotelUpstreamOrderGateway> byChannel = new HashMap<>();
|
||||
|
||||
public HotelUpstreamOrderGatewayRegistry(List<HotelUpstreamOrderGateway> gateways) {
|
||||
if (gateways != null) {
|
||||
for (HotelUpstreamOrderGateway g : gateways) {
|
||||
if (g == null || StrUtil.isBlank(g.channel())) {
|
||||
continue;
|
||||
}
|
||||
byChannel.put(SmHotelDataSource.normalize(g.channel()).toLowerCase(), g);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 落库/展示用的渠道键:local / shangyou / 其它 sheet_name */
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找网关。本地返回 empty;空白历史按商游;未注册渠道返回 empty(配合 {@link #isUnsupportedChannel})。
|
||||
*/
|
||||
public Optional<HotelUpstreamOrderGateway> find(String hotelSheetName) {
|
||||
if (SmHotelDataSource.isLocal(hotelSheetName)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String key = resolveChannelKey(hotelSheetName);
|
||||
return Optional.ofNullable(byChannel.get(key.toLowerCase()));
|
||||
}
|
||||
|
||||
/** 非本地且未注册实现的渠道 */
|
||||
public boolean isUnsupportedChannel(String hotelSheetName) {
|
||||
if (SmHotelDataSource.isLocal(hotelSheetName)) {
|
||||
return false;
|
||||
}
|
||||
String key = resolveChannelKey(hotelSheetName);
|
||||
return !byChannel.containsKey(key.toLowerCase());
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package com.bsd.pjcloud.supermarket.integration.hotel.upstream;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.bsd.pjcloud.supermarket.integration.shangyou.ShangyouOrderCallResult;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 上游订单调用的统一结果(渠道无关语义,供履约/补偿/流水复用)。
|
||||
*/
|
||||
@Data
|
||||
public class HotelUpstreamOrderResult {
|
||||
|
||||
/** HTTP/网络超时、5xx 等,未拿到确定业务结果 */
|
||||
private boolean uncertain;
|
||||
|
||||
/** 业务码,如 200 / 3028 */
|
||||
private String code;
|
||||
|
||||
private String message;
|
||||
|
||||
/** content 对象(orderId/bookStatus 等) */
|
||||
private JSONObject content;
|
||||
|
||||
private JSONObject raw;
|
||||
|
||||
private Integer httpStatus;
|
||||
|
||||
private String exceptionMsg;
|
||||
|
||||
public boolean isBizOk() {
|
||||
return !uncertain && ("200".equals(code) || "0".equals(code) || "OK".equalsIgnoreCase(code));
|
||||
}
|
||||
|
||||
/** 总对总付款失败,文档要求走补偿 */
|
||||
public boolean isPayFail3028() {
|
||||
return "3028".equals(code);
|
||||
}
|
||||
|
||||
public String orderId() {
|
||||
return content == null ? null : content.getString("orderId");
|
||||
}
|
||||
|
||||
public String bookStatus() {
|
||||
return content == null ? null : content.getString("bookStatus");
|
||||
}
|
||||
|
||||
public String payStatus() {
|
||||
return content == null ? null : content.getString("payStatus");
|
||||
}
|
||||
|
||||
public static HotelUpstreamOrderResult uncertain(String msg, Integer httpStatus) {
|
||||
HotelUpstreamOrderResult r = new HotelUpstreamOrderResult();
|
||||
r.setUncertain(true);
|
||||
r.setMessage(msg);
|
||||
r.setHttpStatus(httpStatus);
|
||||
r.setExceptionMsg(msg);
|
||||
return r;
|
||||
}
|
||||
|
||||
public static HotelUpstreamOrderResult fail(String code, String message) {
|
||||
HotelUpstreamOrderResult r = new HotelUpstreamOrderResult();
|
||||
r.setUncertain(false);
|
||||
r.setCode(code);
|
||||
r.setMessage(message);
|
||||
return r;
|
||||
}
|
||||
|
||||
/** 兼容现有商游客户端结果 */
|
||||
public static HotelUpstreamOrderResult fromShangyou(ShangyouOrderCallResult src) {
|
||||
if (src == null) {
|
||||
return uncertain("上游无响应", null);
|
||||
}
|
||||
HotelUpstreamOrderResult r = new HotelUpstreamOrderResult();
|
||||
r.setUncertain(src.isUncertain());
|
||||
r.setCode(src.getCode());
|
||||
r.setMessage(src.getMessage());
|
||||
r.setContent(src.getContent());
|
||||
r.setRaw(src.getRaw());
|
||||
r.setHttpStatus(src.getHttpStatus());
|
||||
r.setExceptionMsg(src.getExceptionMsg());
|
||||
return r;
|
||||
}
|
||||
|
||||
public static HotelUpstreamOrderResult ofCancel(boolean ok, String message) {
|
||||
HotelUpstreamOrderResult r = new HotelUpstreamOrderResult();
|
||||
r.setUncertain(false);
|
||||
r.setCode(ok ? "200" : "FAIL");
|
||||
r.setMessage(StrUtil.blankToDefault(message, ok ? "ok" : "cancel failed"));
|
||||
JSONObject content = new JSONObject();
|
||||
content.put("cancelled", ok);
|
||||
r.setContent(content);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.bsd.pjcloud.supermarket.integration.hotel.upstream;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.bsd.pjcloud.supermarket.constant.SmHotelDataSource;
|
||||
import com.bsd.pjcloud.supermarket.integration.shangyou.ShangyouHotelClient;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 商游(玉编电云)上游订单网关。
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ShangyouHotelUpstreamGateway implements HotelUpstreamOrderGateway {
|
||||
|
||||
private final ShangyouHotelClient client;
|
||||
|
||||
@Override
|
||||
public String channel() {
|
||||
return SmHotelDataSource.SHANGYOU;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject checkAvailability(JSONObject body) {
|
||||
return client.checkAvailability(body);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HotelUpstreamOrderResult createOrder(JSONObject body) {
|
||||
return HotelUpstreamOrderResult.fromShangyou(client.createOrderRaw(body));
|
||||
}
|
||||
|
||||
@Override
|
||||
public HotelUpstreamOrderResult queryByRequestNo(String requestNo) {
|
||||
return HotelUpstreamOrderResult.fromShangyou(client.queryByRequestNoRaw(requestNo));
|
||||
}
|
||||
|
||||
@Override
|
||||
public HotelUpstreamOrderResult queryOrder(String platformNo, String hotelCode) {
|
||||
try {
|
||||
JSONObject content = client.queryOrder(platformNo, hotelCode);
|
||||
HotelUpstreamOrderResult r = new HotelUpstreamOrderResult();
|
||||
r.setUncertain(false);
|
||||
r.setCode("200");
|
||||
r.setContent(content);
|
||||
r.setRaw(content);
|
||||
return r;
|
||||
}
|
||||
catch (Exception e) {
|
||||
return HotelUpstreamOrderResult.uncertain(e.getMessage(), null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HotelUpstreamOrderResult cancelOrder(String platformNo, String hotelCode) {
|
||||
try {
|
||||
boolean ok = client.cancelOrder(platformNo, hotelCode);
|
||||
return HotelUpstreamOrderResult.ofCancel(ok, ok ? "取消成功" : "上游取消返回失败");
|
||||
}
|
||||
catch (Exception e) {
|
||||
return HotelUpstreamOrderResult.uncertain(e.getMessage(), null);
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
-30
@@ -8,6 +8,7 @@ import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.bsd.pjcloud.supermarket.config.ShangyouHotelProperties;
|
||||
import com.bsd.pjcloud.common.core.exception.CheckedException;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelConfigService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
@@ -30,28 +31,35 @@ import org.springframework.data.redis.core.ZSetOperations;
|
||||
|
||||
/**
|
||||
* 玉编电云 CRS 客户端:Basic 换 Token,业务请求 Bearer + cardId/cardPassWord。
|
||||
* <p>
|
||||
* 运行时参数优先读库表渠道对接配置(热更新),未配置时回落 YAML/Nacos。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ShangyouHotelClient {
|
||||
|
||||
private final ShangyouHotelProperties properties;
|
||||
private final SmHotelChannelConfigService channelConfigService;
|
||||
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
|
||||
/** 库表覆盖后的生效配置,保存后台对接配置后立即生效 */
|
||||
private ShangyouHotelProperties props() {
|
||||
return channelConfigService.resolveShangyouRuntime();
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return properties.isEnabled();
|
||||
return props().isEnabled();
|
||||
}
|
||||
|
||||
public void ensureEnabled() {
|
||||
if (!properties.isEnabled()) {
|
||||
throw new CheckedException("上游酒店未启用,请配置 shangyou-hotel.enabled=true");
|
||||
if (!props().isEnabled()) {
|
||||
throw new CheckedException("上游酒店未启用:请在「渠道对接配置」启用商游,或配置 shangyou-hotel.enabled=true");
|
||||
}
|
||||
if (StrUtil.hasBlank(properties.resolveAuthBaseUrl(), properties.resolveBizBaseUrl(), properties.getClientId(),
|
||||
properties.getClientSecret(), properties.getCardId(), properties.getCardPassword())) {
|
||||
if (StrUtil.hasBlank(props().resolveAuthBaseUrl(), props().resolveBizBaseUrl(), props().getClientId(),
|
||||
props().getClientSecret(), props().getCardId(), props().getCardPassword())) {
|
||||
throw new CheckedException(
|
||||
"上游酒店配置不完整:至少配置 base-url(或 auth-base-url+biz-base-url)以及 clientId/secret、cardId/password");
|
||||
"上游酒店配置不完整:请在「渠道对接配置」填写 baseUrl/clientId/secret/cardId/password,或配置 YAML 默认值");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +113,7 @@ public class ShangyouHotelClient {
|
||||
if (StrUtil.isNotBlank(reused)) {
|
||||
return reused;
|
||||
}
|
||||
long lockTtlSec = Math.max(45L, properties.getTimeoutMs() / 1000L + 15L);
|
||||
long lockTtlSec = Math.max(45L, props().getTimeoutMs() / 1000L + 15L);
|
||||
for (int attempt = 0; attempt < 4; attempt++) {
|
||||
Boolean locked = redisTemplate.opsForValue()
|
||||
.setIfAbsent(ShangyouHotelApi.REDIS_TOKEN_LOCK, "1", lockTtlSec, TimeUnit.SECONDS);
|
||||
@@ -217,9 +225,9 @@ public class ShangyouHotelClient {
|
||||
}
|
||||
|
||||
private String fetchAndCacheToken() {
|
||||
String url = properties.resolveAuthBaseUrl() + ShangyouHotelApi.OAUTH_TOKEN;
|
||||
String url = props().resolveAuthBaseUrl() + ShangyouHotelApi.OAUTH_TOKEN;
|
||||
String basic = Base64.getEncoder()
|
||||
.encodeToString((properties.getClientId() + ":" + properties.getClientSecret())
|
||||
.encodeToString((props().getClientId() + ":" + props().getClientSecret())
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
log.info("shangyou oauth token url={}", url);
|
||||
try (HttpResponse response = HttpRequest.post(url)
|
||||
@@ -228,7 +236,7 @@ public class ShangyouHotelClient {
|
||||
.header("Accept-Encoding", "gzip, deflate")
|
||||
.header("Content-Type", "application/json;charset=utf-8")
|
||||
.header("Connection", "Keep-Alive")
|
||||
.timeout(properties.getTimeoutMs())
|
||||
.timeout(props().getTimeoutMs())
|
||||
.execute()) {
|
||||
String body = response.body();
|
||||
if (!response.isOk()) {
|
||||
@@ -238,14 +246,14 @@ public class ShangyouHotelClient {
|
||||
String token = firstNonBlank(resp.getString("access_token"), resp.getString("accessToken"));
|
||||
if (StrUtil.isBlank(token)) {
|
||||
throw new CheckedException("上游鉴权未返回 access_token: " + body + "(请核对 auth 地址是否为 "
|
||||
+ properties.resolveAuthBaseUrl() + "/oauth/token )");
|
||||
+ props().resolveAuthBaseUrl() + "/oauth/token )");
|
||||
}
|
||||
long expires = resp.getLongValue("expires_in");
|
||||
if (expires <= 0) {
|
||||
expires = 21600L;
|
||||
}
|
||||
// 文档建议缓存约 5 小时 58 分:expires_in(21600) - skew(默认120秒)
|
||||
long ttl = Math.max(60L, expires - properties.getTokenSkewSeconds());
|
||||
long ttl = Math.max(60L, expires - props().getTokenSkewSeconds());
|
||||
redisTemplate.opsForValue().set(ShangyouHotelApi.REDIS_TOKEN_KEY, token, ttl, TimeUnit.SECONDS);
|
||||
redisTemplate.opsForValue()
|
||||
.set(ShangyouHotelApi.REDIS_TOKEN_REFRESHED_AT, String.valueOf(System.currentTimeMillis()), ttl,
|
||||
@@ -400,9 +408,9 @@ public class ShangyouHotelClient {
|
||||
JSONArray arr = asArray(resp.get("content"));
|
||||
resp.remove("content");
|
||||
int count = arr.size();
|
||||
if (count >= properties.getHotelListWarnSize()) {
|
||||
if (count >= props().getHotelListWarnSize()) {
|
||||
log.warn("|上游酒店列表|数量较大|count={}|warnSize={}|同步期会有短时内存峰值,建议低峰强制刷新|", count,
|
||||
properties.getHotelListWarnSize());
|
||||
props().getHotelListWarnSize());
|
||||
}
|
||||
String tmpHash = ShangyouHotelApi.REDIS_HOTEL_HASH_KEY + ":tmp";
|
||||
String tmpZset = ShangyouHotelApi.REDIS_HOTEL_ZSET_KEY + ":tmp";
|
||||
@@ -446,7 +454,7 @@ public class ShangyouHotelClient {
|
||||
redisTemplate.rename(tmpHash, ShangyouHotelApi.REDIS_HOTEL_HASH_KEY);
|
||||
redisTemplate.rename(tmpZset, ShangyouHotelApi.REDIS_HOTEL_ZSET_KEY);
|
||||
redisTemplate.rename(tmpSet, ShangyouHotelApi.REDIS_UPSTREAM_ID_SET);
|
||||
long ttl = properties.getHotelListCacheMinutes();
|
||||
long ttl = props().getHotelListCacheMinutes();
|
||||
redisTemplate.expire(ShangyouHotelApi.REDIS_HOTEL_HASH_KEY, ttl, TimeUnit.MINUTES);
|
||||
redisTemplate.expire(ShangyouHotelApi.REDIS_HOTEL_ZSET_KEY, ttl, TimeUnit.MINUTES);
|
||||
// ID 集合给差集用,TTL 略长,避免日更空窗
|
||||
@@ -458,7 +466,7 @@ public class ShangyouHotelClient {
|
||||
redisTemplate.delete(tmpSet);
|
||||
}
|
||||
log.info("|上游酒店列表|同步完成|count={}|costMs={}|cacheMinutes={}|store=ZSET+HASH+SET|", count,
|
||||
System.currentTimeMillis() - t0, properties.getHotelListCacheMinutes());
|
||||
System.currentTimeMillis() - t0, props().getHotelListCacheMinutes());
|
||||
}
|
||||
catch (Exception e) {
|
||||
redisTemplate.delete(tmpHash);
|
||||
@@ -529,7 +537,7 @@ public class ShangyouHotelClient {
|
||||
if (!levels.isEmpty()) {
|
||||
body.put("memberLevelList", levels);
|
||||
}
|
||||
String path = StrUtil.blankToDefault(properties.getRoomPricePath(), "/hotel/getRoomPrice");
|
||||
String path = StrUtil.blankToDefault(props().getRoomPricePath(), "/hotel/getRoomPrice");
|
||||
if (!path.startsWith("/")) {
|
||||
path = "/" + path;
|
||||
}
|
||||
@@ -541,7 +549,7 @@ public class ShangyouHotelClient {
|
||||
* 对方约定约 20 QPS;配置 price-api-qps≤0 关闭。
|
||||
*/
|
||||
private void acquirePriceApiPermit() {
|
||||
double qps = properties.getPriceApiQps();
|
||||
double qps = props().getPriceApiQps();
|
||||
if (qps <= 0) {
|
||||
return;
|
||||
}
|
||||
@@ -581,7 +589,7 @@ public class ShangyouHotelClient {
|
||||
if (StrUtil.isBlank(path)) {
|
||||
return false;
|
||||
}
|
||||
String configured = StrUtil.blankToDefault(properties.getRoomPricePath(), "/hotel/getRoomPrice");
|
||||
String configured = StrUtil.blankToDefault(props().getRoomPricePath(), "/hotel/getRoomPrice");
|
||||
String p = path.startsWith("/") ? path : "/" + path;
|
||||
String c = configured.startsWith("/") ? configured : "/" + configured;
|
||||
return p.equalsIgnoreCase(c) || p.toLowerCase().contains("getroomprice");
|
||||
@@ -613,7 +621,7 @@ public class ShangyouHotelClient {
|
||||
public ShangyouOrderCallResult createOrderRaw(JSONObject body) {
|
||||
fillCard(body);
|
||||
if (StrUtil.isBlank(body.getString("bookerId"))) {
|
||||
body.put("bookerId", StrUtil.blankToDefault(properties.getBookerId(), properties.getCardId()));
|
||||
body.put("bookerId", StrUtil.blankToDefault(props().getBookerId(), props().getCardId()));
|
||||
}
|
||||
return postOrderBiz(ShangyouHotelApi.ORDER_ADD, body);
|
||||
}
|
||||
@@ -689,7 +697,7 @@ public class ShangyouHotelClient {
|
||||
catch (Exception e) {
|
||||
return ShangyouOrderCallResult.uncertain("取 Token 失败: " + e.getMessage(), null);
|
||||
}
|
||||
String url = properties.resolveBizBaseUrl() + path;
|
||||
String url = props().resolveBizBaseUrl() + path;
|
||||
log.info("shangyou order request path={} retried={} bodyKeys={}", path, retried, body.keySet());
|
||||
try (HttpResponse response = HttpRequest.post(url)
|
||||
.header("Authorization", "Bearer " + token)
|
||||
@@ -697,7 +705,7 @@ public class ShangyouHotelClient {
|
||||
.header("Accept-Encoding", "gzip, deflate")
|
||||
.header("Content-Type", "application/json;charset=UTF-8")
|
||||
.body(body.toJSONString())
|
||||
.timeout(properties.getTimeoutMs())
|
||||
.timeout(props().getTimeoutMs())
|
||||
.execute()) {
|
||||
String respBody = response.body();
|
||||
int status = response.getStatus();
|
||||
@@ -746,7 +754,7 @@ public class ShangyouHotelClient {
|
||||
}
|
||||
|
||||
public String resolveCallbackSecret() {
|
||||
return StrUtil.blankToDefault(properties.getCallbackSecret(), properties.getCardPassword());
|
||||
return StrUtil.blankToDefault(props().getCallbackSecret(), props().getCardPassword());
|
||||
}
|
||||
|
||||
private JSONObject baseCardBody() {
|
||||
@@ -756,15 +764,15 @@ public class ShangyouHotelClient {
|
||||
}
|
||||
|
||||
private void fillCard(JSONObject body) {
|
||||
body.put("cardId", properties.getCardId());
|
||||
body.put("cardPassWord", properties.getCardPassword());
|
||||
body.put("cardId", props().getCardId());
|
||||
body.put("cardPassWord", props().getCardPassword());
|
||||
}
|
||||
|
||||
private List<String> parseMemberLevels() {
|
||||
if (StrUtil.isBlank(properties.getMemberLevels())) {
|
||||
if (StrUtil.isBlank(props().getMemberLevels())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return StrUtil.splitTrim(properties.getMemberLevels(), ',');
|
||||
return StrUtil.splitTrim(props().getMemberLevels(), ',');
|
||||
}
|
||||
|
||||
private JSONObject postBiz(String path, JSONObject body) {
|
||||
@@ -785,7 +793,7 @@ public class ShangyouHotelClient {
|
||||
acquirePriceApiPermit();
|
||||
}
|
||||
String token = getAccessToken(forceToken, invalidToken);
|
||||
String url = properties.resolveBizBaseUrl() + path;
|
||||
String url = props().resolveBizBaseUrl() + path;
|
||||
log.info("shangyou request path={} retryRound={} forceToken={} bodyKeys={}", path, retryRound, forceToken,
|
||||
body.keySet());
|
||||
try (HttpResponse response = HttpRequest.post(url)
|
||||
@@ -794,7 +802,7 @@ public class ShangyouHotelClient {
|
||||
.header("Accept-Encoding", "gzip, deflate")
|
||||
.header("Content-Type", "application/json;charset=UTF-8")
|
||||
.body(body.toJSONString())
|
||||
.timeout(properties.getTimeoutMs())
|
||||
.timeout(props().getTimeoutMs())
|
||||
.execute()) {
|
||||
String respBody = response.body();
|
||||
boolean httpUnauthorized = response.getStatus() == 401;
|
||||
|
||||
+10
-5
@@ -1,6 +1,8 @@
|
||||
package com.bsd.pjcloud.supermarket.integration.shangyou;
|
||||
|
||||
import com.bsd.pjcloud.supermarket.config.ShangyouHotelProperties;
|
||||
import com.bsd.pjcloud.supermarket.constant.SmHotelDataSource;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelConfigService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelOrderService;
|
||||
import com.bsd.pjcloud.common.data.tenant.TenantBroker;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
@@ -14,6 +16,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* 订单补偿调度:按文档在下单后第 3/5/10 分钟 queryByRequestNo(必要时重试 order/add)。
|
||||
* 启用状态读库表热配置,后台开关后无需重启 Worker。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@@ -22,6 +25,8 @@ public class ShangyouOrderCompensateWorker implements ApplicationRunner {
|
||||
|
||||
private final ShangyouHotelProperties properties;
|
||||
|
||||
private final SmHotelChannelConfigService channelConfigService;
|
||||
|
||||
private final SmHotelOrderService orderService;
|
||||
|
||||
private final AtomicBoolean running = new AtomicBoolean(true);
|
||||
@@ -30,19 +35,19 @@ public class ShangyouOrderCompensateWorker implements ApplicationRunner {
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
if (!properties.isEnabled()) {
|
||||
log.info("|订单补偿|上游未启用,Worker 不启动");
|
||||
return;
|
||||
}
|
||||
worker = new Thread(this::loop, "shangyou-order-compensate");
|
||||
worker.setDaemon(true);
|
||||
worker.start();
|
||||
log.info("|订单补偿|Worker 已启动(轮询 next_compensate_time)");
|
||||
log.info("|订单补偿|Worker 已启动(轮询 next_compensate_time;渠道启用热感知)");
|
||||
}
|
||||
|
||||
private void loop() {
|
||||
while (running.get()) {
|
||||
try {
|
||||
if (!channelConfigService.isChannelEnabled(SmHotelDataSource.SHANGYOU)) {
|
||||
Thread.sleep(30000L);
|
||||
continue;
|
||||
}
|
||||
Long tenantId = properties.getCallbackTenantId() == null ? 1L : properties.getCallbackTenantId();
|
||||
int n = TenantBroker.applyAs(tenantId, tid -> orderService.processDueCompensate(20));
|
||||
if (n > 0) {
|
||||
|
||||
+18
-6
@@ -2,6 +2,7 @@ package com.bsd.pjcloud.supermarket.job;
|
||||
|
||||
import com.bsd.pjcloud.common.data.tenant.TenantBroker;
|
||||
import com.bsd.pjcloud.supermarket.config.ShangyouHotelProperties;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelConfigService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelOrderService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelSyncService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -10,24 +11,31 @@ import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* YAML 可配 Cron 的日更入口(与 XXL Job 二选一即可)。
|
||||
* YAML / 库表可配 Cron 的日更入口(与 XXL Job 二选一即可)。
|
||||
* <p>
|
||||
* 开关:{@code shangyou-hotel.spring-schedule-enabled}<br>
|
||||
* 时间:{@code hotel-list-daily-cron} / {@code price-roll-daily-cron} / {@code local-daily-roll-cron}
|
||||
* Cron 表达式本身仍读 YAML(改触发时刻需重启);是否执行读库表热配置
|
||||
* {@code springScheduleEnabled} / 各项日更开关,测试环境关闭即可,无需重启。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class SmHotelShangyouSpringSchedule {
|
||||
|
||||
private final ShangyouHotelProperties properties;
|
||||
private final ShangyouHotelProperties yamlProperties;
|
||||
|
||||
private final SmHotelChannelConfigService channelConfigService;
|
||||
|
||||
private final SmHotelSyncService syncService;
|
||||
|
||||
private final SmHotelOrderService hotelOrderService;
|
||||
|
||||
private ShangyouHotelProperties props() {
|
||||
return channelConfigService.resolveShangyouRuntime();
|
||||
}
|
||||
|
||||
@Scheduled(cron = "${shangyou-hotel.hotel-list-daily-cron:0 0 2 * * ?}")
|
||||
public void scheduledListSync() {
|
||||
ShangyouHotelProperties properties = props();
|
||||
if (!properties.isSpringScheduleEnabled()) {
|
||||
return;
|
||||
}
|
||||
@@ -45,6 +53,7 @@ public class SmHotelShangyouSpringSchedule {
|
||||
|
||||
@Scheduled(cron = "${shangyou-hotel.price-roll-daily-cron:0 0 3 * * ?}")
|
||||
public void scheduledPriceRoll() {
|
||||
ShangyouHotelProperties properties = props();
|
||||
if (!properties.isSpringScheduleEnabled()) {
|
||||
return;
|
||||
}
|
||||
@@ -62,6 +71,7 @@ public class SmHotelShangyouSpringSchedule {
|
||||
|
||||
@Scheduled(cron = "${shangyou-hotel.local-daily-roll-cron:0 30 3 * * ?}")
|
||||
public void scheduledLocalDailyRoll() {
|
||||
ShangyouHotelProperties properties = props();
|
||||
if (!properties.isSpringScheduleEnabled()) {
|
||||
return;
|
||||
}
|
||||
@@ -80,12 +90,14 @@ public class SmHotelShangyouSpringSchedule {
|
||||
/**
|
||||
* App 待支付超时清理:默认每分钟扫一次,取消过期单并释放本地预占库存。
|
||||
* 不依赖 spring-schedule-enabled(支付超时与日更无关)。
|
||||
* 租户与订单补偿 Worker 一致,走 callback-tenant-id。
|
||||
*/
|
||||
@Scheduled(fixedDelayString = "${shangyou-hotel.app-pay-expire-scan-ms:60000}")
|
||||
public void scheduledCancelExpiredAppUnpaid() {
|
||||
try {
|
||||
Long tenantId = properties.getCallbackTenantId() == null ? 1L : properties.getCallbackTenantId();
|
||||
ShangyouHotelProperties properties = props();
|
||||
Long tenantId = properties.getCallbackTenantId() == null
|
||||
? (yamlProperties.getCallbackTenantId() == null ? 1L : yamlProperties.getCallbackTenantId())
|
||||
: properties.getCallbackTenantId();
|
||||
TenantBroker.runAs(tenantId, tid -> hotelOrderService.cancelExpiredAppUnpaidOrders(100));
|
||||
}
|
||||
catch (Exception e) {
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.bsd.pjcloud.supermarket.mapper;
|
||||
|
||||
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelChannelConfigEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface SmHotelChannelConfigMapper extends PjcloudBaseMapper<SmHotelChannelConfigEntity> {
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.bsd.pjcloud.supermarket.mapper;
|
||||
|
||||
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelChannelDeptEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface SmHotelChannelDeptMapper extends PjcloudBaseMapper<SmHotelChannelDeptEntity> {
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.bsd.pjcloud.supermarket.mapper;
|
||||
|
||||
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelDownstreamPushLogEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface SmHotelDownstreamPushLogMapper extends PjcloudBaseMapper<SmHotelDownstreamPushLogEntity> {
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.bsd.pjcloud.supermarket.mapper;
|
||||
|
||||
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelOrderUpstreamLogEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface SmHotelOrderUpstreamLogMapper extends PjcloudBaseMapper<SmHotelOrderUpstreamLogEntity> {
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package com.bsd.pjcloud.supermarket.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.supermarket.config.HotelDownstreamProperties;
|
||||
import com.bsd.pjcloud.supermarket.config.ShangyouHotelProperties;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelChannelConfigEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SmHotelChannelConfigService extends IService<SmHotelChannelConfigEntity> {
|
||||
|
||||
Page<SmHotelChannelConfigEntity> pageConfigs(Page<SmHotelChannelConfigEntity> page, String channel,
|
||||
String direction);
|
||||
|
||||
/** 默认查上游 */
|
||||
SmHotelChannelConfigEntity getByChannel(String channel);
|
||||
|
||||
SmHotelChannelConfigEntity getByChannel(String channel, String direction);
|
||||
|
||||
R<Boolean> saveOrUpdateByChannel(SmHotelChannelConfigEntity entity);
|
||||
|
||||
R<Boolean> removeByIdsSafe(Long[] ids);
|
||||
|
||||
/** 清缓存,下次调用重新读库 */
|
||||
void evictCache(String channel);
|
||||
|
||||
/**
|
||||
* 商游运行时配置:库表覆盖 YAML 默认;无库表行则纯 YAML。
|
||||
* 热更新:保存后 {@link #evictCache},无需重启。
|
||||
*/
|
||||
ShangyouHotelProperties resolveShangyouRuntime();
|
||||
|
||||
/** 下游伙伴运行时配置(库表覆盖 YAML 默认) */
|
||||
HotelDownstreamProperties resolveDownstreamRuntime(String partnerCode);
|
||||
|
||||
/** 已启用的下游伙伴编码列表 */
|
||||
List<String> listEnabledDownstreamPartners();
|
||||
|
||||
/** 渠道是否在库表中启用(无行时回落 YAML enabled;默认按上游) */
|
||||
boolean isChannelEnabled(String channel);
|
||||
|
||||
boolean isChannelEnabled(String channel, String direction);
|
||||
|
||||
/** YAML/Nacos 默认快照,供后台表单对照 */
|
||||
ShangyouHotelProperties yamlDefaultsSnapshot();
|
||||
|
||||
HotelDownstreamProperties downstreamYamlDefaultsSnapshot();
|
||||
|
||||
/** 已实现的下游协议列表(后台下拉) */
|
||||
java.util.List<String> listDownstreamProtocols();
|
||||
|
||||
/** 已配置的下游伙伴(范围映射下拉,编码只在对接配置维护一次) */
|
||||
java.util.List<SmHotelChannelConfigEntity> listDownstreamPartners();
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.bsd.pjcloud.supermarket.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelChannelDeptEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SmHotelChannelDeptService extends IService<SmHotelChannelDeptEntity> {
|
||||
|
||||
Page<SmHotelChannelDeptEntity> pageMappings(Page<SmHotelChannelDeptEntity> page, SmHotelChannelDeptEntity query);
|
||||
|
||||
List<SmHotelChannelDeptEntity> listEnabled();
|
||||
|
||||
R<Boolean> saveMapping(SmHotelChannelDeptEntity entity);
|
||||
|
||||
R<Boolean> updateMapping(SmHotelChannelDeptEntity entity);
|
||||
|
||||
R<Boolean> removeMappings(Long[] ids);
|
||||
|
||||
/** 上游渠道 → 归属部门(仅 direction=upstream) */
|
||||
Long resolveDeptId(String channel);
|
||||
|
||||
R<Long> requireEnabledDeptId(String channel);
|
||||
|
||||
/**
|
||||
* 下游:按酒店匹配应推送的伙伴。
|
||||
* <ul>
|
||||
* <li>scope=dept:部门 + 可选区域</li>
|
||||
* <li>scope=hotel:hotelIds 包含该酒店</li>
|
||||
* </ul>
|
||||
*/
|
||||
List<String> listDownstreamPartnersForHotel(Long hotelId, Long deptId, Long districtAdcode, String district);
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.bsd.pjcloud.supermarket.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelDownstreamPushLogEntity;
|
||||
|
||||
public interface SmHotelDownstreamPushLogService extends IService<SmHotelDownstreamPushLogEntity> {
|
||||
|
||||
Page<SmHotelDownstreamPushLogEntity> pageLogs(Page<SmHotelDownstreamPushLogEntity> page,
|
||||
SmHotelDownstreamPushLogEntity query);
|
||||
|
||||
/** 异步落库推送流水(失败不影响主流程) */
|
||||
void record(SmHotelDownstreamPushLogEntity logEntity);
|
||||
}
|
||||
+5
@@ -4,11 +4,13 @@ import cn.hutool.json.JSONObject;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.common.security.service.PjcloudUser;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelOrderEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelOrderUpstreamLogEntity;
|
||||
import com.bsd.pjcloud.supermarket.vo.SmHotelOrderDetailVo;
|
||||
import com.bsd.pjcloud.supermarket.vo.SmHotelOrderPayInfoVo;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface SmHotelOrderService {
|
||||
@@ -96,4 +98,7 @@ public interface SmHotelOrderService {
|
||||
* 预订单状态变更回调落库(验签后由上层调用)。
|
||||
*/
|
||||
Map<String, Object> handleOrderStatusNotify(Map<String, Object> body);
|
||||
|
||||
/** 上游调用流水(按时间升序) */
|
||||
List<SmHotelOrderUpstreamLogEntity> listUpstreamLogs(Long orderId);
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.bsd.pjcloud.supermarket.service;
|
||||
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelOrderUpstreamLogEntity;
|
||||
import com.bsd.pjcloud.supermarket.integration.hotel.upstream.HotelUpstreamOrderResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SmHotelOrderUpstreamLogService {
|
||||
|
||||
void saveCall(Long orderId, String channel, String action, HotelUpstreamOrderResult result, String reqJson,
|
||||
long costMs);
|
||||
|
||||
void saveSimple(Long orderId, String channel, String action, boolean success, String bizCode, String message,
|
||||
String reqJson, String respJson, Long costMs);
|
||||
|
||||
List<SmHotelOrderUpstreamLogEntity> listByOrderId(Long orderId);
|
||||
}
|
||||
+2
-1
@@ -40,8 +40,9 @@ public interface SmHotelSyncService {
|
||||
/**
|
||||
* 异步拉取全部启用酒店详情(房型+产品+未来 price-days 房价)。
|
||||
* @param limit 最多处理家数,null/≤0 用配置 detail-pull-max-hotels(0=不限)
|
||||
* @param dataSource 第三方渠道(须已在「渠道部门」维护);只拉取该渠道酒店并归属对应部门
|
||||
*/
|
||||
R<SmHotelSyncLogEntity> startDetailPullAsync(Integer limit, String triggerType);
|
||||
R<SmHotelSyncLogEntity> startDetailPullAsync(Integer limit, String triggerType, String dataSource);
|
||||
|
||||
/**
|
||||
* 根据某次同步日志的 failDetail,只重试失败酒店(支持 DETAIL_PULL / PRICE_ROLL)。
|
||||
|
||||
+474
@@ -0,0 +1,474 @@
|
||||
package com.bsd.pjcloud.supermarket.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.supermarket.config.HotelDownstreamProperties;
|
||||
import com.bsd.pjcloud.supermarket.config.ShangyouHotelProperties;
|
||||
import com.bsd.pjcloud.supermarket.constant.SmHotelChannelDirection;
|
||||
import com.bsd.pjcloud.supermarket.constant.SmHotelDataSource;
|
||||
import com.bsd.pjcloud.supermarket.constant.SmHotelDownstreamProtocol;
|
||||
import com.bsd.pjcloud.supermarket.dto.SmHotelDownstreamChannelConfigDTO;
|
||||
import com.bsd.pjcloud.supermarket.dto.SmHotelShangyouChannelConfigDTO;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelChannelConfigEntity;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelChannelConfigMapper;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelConfigService;
|
||||
import com.bsd.pjcloud.supermarket.util.ChannelConfigTextParser;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 渠道对接参数:库表覆盖 YAML,进程内缓存,保存后立即失效无需重启。
|
||||
* direction=upstream 上游;direction=downstream 下游销售方(可多家)。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SmHotelChannelConfigServiceImpl extends ServiceImpl<SmHotelChannelConfigMapper, SmHotelChannelConfigEntity>
|
||||
implements SmHotelChannelConfigService {
|
||||
|
||||
private final ShangyouHotelProperties yamlDefaults;
|
||||
|
||||
private final HotelDownstreamProperties downstreamYamlDefaults;
|
||||
|
||||
/** channel → 合并后的商游运行时配置(仅 shangyou 上游) */
|
||||
private final Map<String, ShangyouHotelProperties> shangyouRuntimeCache = new ConcurrentHashMap<>();
|
||||
|
||||
/** partnerCode → 下游运行时 */
|
||||
private final Map<String, HotelDownstreamProperties> downstreamRuntimeCache = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public Page<SmHotelChannelConfigEntity> pageConfigs(Page<SmHotelChannelConfigEntity> page, String channel,
|
||||
String direction) {
|
||||
String dir = StrUtil.isBlank(direction) ? null : SmHotelChannelDirection.normalize(direction);
|
||||
return baseMapper.selectPage(page, Wrappers.<SmHotelChannelConfigEntity>lambdaQuery()
|
||||
.eq(StrUtil.isNotBlank(channel), SmHotelChannelConfigEntity::getChannel,
|
||||
normalizeChannelCode(channel))
|
||||
.eq(dir != null, SmHotelChannelConfigEntity::getDirection, dir)
|
||||
.orderByDesc(SmHotelChannelConfigEntity::getUpdateTime)
|
||||
.orderByDesc(SmHotelChannelConfigEntity::getCreateTime));
|
||||
}
|
||||
|
||||
@Override
|
||||
public SmHotelChannelConfigEntity getByChannel(String channel) {
|
||||
return getByChannel(channel, SmHotelChannelDirection.UPSTREAM);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SmHotelChannelConfigEntity getByChannel(String channel, String direction) {
|
||||
String ch = normalizeChannelCode(channel);
|
||||
if (ch.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String dir = SmHotelChannelDirection.normalize(direction);
|
||||
return baseMapper.selectOne(Wrappers.<SmHotelChannelConfigEntity>lambdaQuery()
|
||||
.eq(SmHotelChannelConfigEntity::getChannel, ch)
|
||||
.eq(SmHotelChannelConfigEntity::getDirection, dir)
|
||||
.last("limit 1"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<Boolean> saveOrUpdateByChannel(SmHotelChannelConfigEntity entity) {
|
||||
if (entity == null) {
|
||||
return R.failed("参数不能为空");
|
||||
}
|
||||
String dir = SmHotelChannelDirection.normalize(entity.getDirection());
|
||||
entity.setDirection(dir);
|
||||
String ch = normalizeChannelCode(entity.getChannel());
|
||||
if (ch.isEmpty()) {
|
||||
return R.failed(SmHotelChannelDirection.isDownstream(dir) ? "请填写下游伙伴编码" : "请选择渠道");
|
||||
}
|
||||
if (SmHotelChannelDirection.isUpstream(dir) && SmHotelDataSource.isLocal(ch)) {
|
||||
return R.failed("本地渠道无需对接配置");
|
||||
}
|
||||
entity.setChannel(ch);
|
||||
if (SmHotelChannelDirection.isDownstream(dir)) {
|
||||
String protocol = SmHotelDownstreamProtocol.blankToDefault(entity.getProtocol());
|
||||
if (!SmHotelDownstreamProtocol.isSupported(protocol)) {
|
||||
return R.failed("不支持的下游协议「" + entity.getProtocol() + "」,当前已实现:"
|
||||
+ String.join(",", SmHotelDownstreamProtocol.supported()));
|
||||
}
|
||||
entity.setProtocol(protocol);
|
||||
}
|
||||
else {
|
||||
// 上游:协议可空或与渠道同名,不强校验
|
||||
if (StrUtil.isBlank(entity.getProtocol())) {
|
||||
entity.setProtocol(ch);
|
||||
}
|
||||
}
|
||||
if (StrUtil.isBlank(entity.getEnabled())) {
|
||||
entity.setEnabled("0");
|
||||
}
|
||||
if (StrUtil.isNotBlank(entity.getConfigJson())) {
|
||||
R<String> textCheck = validateConfigText(entity.getConfigJson());
|
||||
if (textCheck != null) {
|
||||
return R.failed(textCheck.getMsg());
|
||||
}
|
||||
}
|
||||
SmHotelChannelConfigEntity existing = getByChannel(ch, dir);
|
||||
boolean ok;
|
||||
if (existing != null) {
|
||||
entity.setId(existing.getId());
|
||||
ok = baseMapper.updateById(entity) > 0;
|
||||
}
|
||||
else {
|
||||
ok = baseMapper.insert(entity) > 0;
|
||||
}
|
||||
if (ok) {
|
||||
evictCache(ch);
|
||||
log.info("|渠道对接配置|已保存并刷新缓存|direction={}|channel={}|enabled={}|", dir, ch, entity.getEnabled());
|
||||
}
|
||||
return R.ok(ok);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<Boolean> removeByIdsSafe(Long[] ids) {
|
||||
if (ids == null || ids.length == 0) {
|
||||
return R.failed("请选择要删除的记录");
|
||||
}
|
||||
for (Long id : ids) {
|
||||
SmHotelChannelConfigEntity row = baseMapper.selectById(id);
|
||||
if (row != null) {
|
||||
evictCache(row.getChannel());
|
||||
}
|
||||
}
|
||||
return R.ok(removeByIds(Arrays.asList(ids)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evictCache(String channel) {
|
||||
String ch = normalizeChannelCode(channel);
|
||||
if (ch.isEmpty()) {
|
||||
shangyouRuntimeCache.clear();
|
||||
downstreamRuntimeCache.clear();
|
||||
return;
|
||||
}
|
||||
shangyouRuntimeCache.remove(ch);
|
||||
downstreamRuntimeCache.remove(ch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ShangyouHotelProperties resolveShangyouRuntime() {
|
||||
return shangyouRuntimeCache.computeIfAbsent(SmHotelDataSource.SHANGYOU, this::buildShangyouRuntime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HotelDownstreamProperties resolveDownstreamRuntime(String partnerCode) {
|
||||
String code = normalizeChannelCode(partnerCode);
|
||||
if (code.isEmpty()) {
|
||||
return copyDownstreamProps(downstreamYamlDefaults);
|
||||
}
|
||||
return downstreamRuntimeCache.computeIfAbsent(code, this::buildDownstreamRuntime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> listEnabledDownstreamPartners() {
|
||||
List<SmHotelChannelConfigEntity> rows = baseMapper.selectList(Wrappers.<SmHotelChannelConfigEntity>lambdaQuery()
|
||||
.eq(SmHotelChannelConfigEntity::getDirection, SmHotelChannelDirection.DOWNSTREAM)
|
||||
.eq(SmHotelChannelConfigEntity::getEnabled, "1")
|
||||
.orderByAsc(SmHotelChannelConfigEntity::getChannel));
|
||||
List<String> codes = rows.stream().map(SmHotelChannelConfigEntity::getChannel).filter(StrUtil::isNotBlank)
|
||||
.map(this::normalizeChannelCode).distinct().collect(Collectors.toList());
|
||||
// 库表无启用下游时,回落旧 YAML(单下游兼容)
|
||||
if (codes.isEmpty() && downstreamYamlDefaults.isEnabled()) {
|
||||
codes.add("default");
|
||||
}
|
||||
return codes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChannelEnabled(String channel) {
|
||||
return isChannelEnabled(channel, SmHotelChannelDirection.UPSTREAM);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChannelEnabled(String channel, String direction) {
|
||||
String ch = normalizeChannelCode(channel);
|
||||
String dir = SmHotelChannelDirection.normalize(direction);
|
||||
if (ch.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
SmHotelChannelConfigEntity row = getByChannel(ch, dir);
|
||||
if (row != null) {
|
||||
return "1".equals(row.getEnabled());
|
||||
}
|
||||
if (SmHotelChannelDirection.isUpstream(dir) && SmHotelDataSource.SHANGYOU.equalsIgnoreCase(ch)) {
|
||||
return yamlDefaults.isEnabled();
|
||||
}
|
||||
if (SmHotelChannelDirection.isDownstream(dir) && "default".equals(ch)) {
|
||||
return downstreamYamlDefaults.isEnabled();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ShangyouHotelProperties yamlDefaultsSnapshot() {
|
||||
return copyProps(yamlDefaults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HotelDownstreamProperties downstreamYamlDefaultsSnapshot() {
|
||||
return copyDownstreamProps(downstreamYamlDefaults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> listDownstreamProtocols() {
|
||||
return SmHotelDownstreamProtocol.supported().stream().toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SmHotelChannelConfigEntity> listDownstreamPartners() {
|
||||
return baseMapper.selectList(Wrappers.<SmHotelChannelConfigEntity>lambdaQuery()
|
||||
.eq(SmHotelChannelConfigEntity::getDirection, SmHotelChannelDirection.DOWNSTREAM)
|
||||
.orderByAsc(SmHotelChannelConfigEntity::getChannel));
|
||||
}
|
||||
|
||||
private ShangyouHotelProperties buildShangyouRuntime(String channel) {
|
||||
ShangyouHotelProperties merged = copyProps(yamlDefaults);
|
||||
SmHotelChannelConfigEntity row = getByChannel(channel, SmHotelChannelDirection.UPSTREAM);
|
||||
if (row == null) {
|
||||
return merged;
|
||||
}
|
||||
merged.setEnabled("1".equals(row.getEnabled()));
|
||||
if (StrUtil.isBlank(row.getConfigJson())) {
|
||||
return merged;
|
||||
}
|
||||
try {
|
||||
SmHotelShangyouChannelConfigDTO dto = ChannelConfigTextParser.parseToShangyouDto(row.getConfigJson());
|
||||
if (dto != null) {
|
||||
overlayShangyou(merged, dto);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|渠道对接配置|解析上游配置失败,回落 YAML|channel={}|err={}|", channel, e.getMessage());
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
private HotelDownstreamProperties buildDownstreamRuntime(String partnerCode) {
|
||||
HotelDownstreamProperties merged = copyDownstreamProps(downstreamYamlDefaults);
|
||||
merged.setProtocol(SmHotelDownstreamProtocol.blankToDefault(merged.getProtocol()));
|
||||
if ("default".equals(partnerCode)) {
|
||||
return merged;
|
||||
}
|
||||
SmHotelChannelConfigEntity row = getByChannel(partnerCode, SmHotelChannelDirection.DOWNSTREAM);
|
||||
if (row == null) {
|
||||
merged.setEnabled(false);
|
||||
return merged;
|
||||
}
|
||||
merged.setEnabled("1".equals(row.getEnabled()));
|
||||
// 库表 protocol 优先;YAML 里的 protocol 可补充覆盖
|
||||
if (StrUtil.isNotBlank(row.getProtocol())) {
|
||||
merged.setProtocol(SmHotelDownstreamProtocol.normalize(row.getProtocol()));
|
||||
}
|
||||
if (StrUtil.isBlank(row.getConfigJson())) {
|
||||
return merged;
|
||||
}
|
||||
try {
|
||||
SmHotelDownstreamChannelConfigDTO dto = ChannelConfigTextParser.parseToDownstreamDto(row.getConfigJson());
|
||||
if (dto != null) {
|
||||
overlayDownstream(merged, dto);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|渠道对接配置|解析下游配置失败,回落 YAML|partner={}|err={}|", partnerCode, e.getMessage());
|
||||
}
|
||||
merged.setProtocol(SmHotelDownstreamProtocol.blankToDefault(merged.getProtocol()));
|
||||
return merged;
|
||||
}
|
||||
|
||||
private void overlayDownstream(HotelDownstreamProperties target, SmHotelDownstreamChannelConfigDTO dto) {
|
||||
if (StrUtil.isNotBlank(dto.getProtocol())) {
|
||||
target.setProtocol(SmHotelDownstreamProtocol.normalize(dto.getProtocol()));
|
||||
}
|
||||
if (StrUtil.isNotBlank(dto.getBaseUrl())) {
|
||||
target.setBaseUrl(dto.getBaseUrl().trim());
|
||||
}
|
||||
if (StrUtil.isNotBlank(dto.getAppKey())) {
|
||||
target.setAppKey(dto.getAppKey().trim());
|
||||
}
|
||||
if (StrUtil.isNotBlank(dto.getAppSecret())) {
|
||||
target.setAppSecret(dto.getAppSecret().trim());
|
||||
}
|
||||
if (dto.getTimeoutMs() != null && dto.getTimeoutMs() > 0) {
|
||||
target.setTimeoutMs(dto.getTimeoutMs());
|
||||
}
|
||||
if (dto.getDailyDays() != null && dto.getDailyDays() > 0) {
|
||||
target.setDailyDays(dto.getDailyDays());
|
||||
}
|
||||
}
|
||||
|
||||
private void overlayShangyou(ShangyouHotelProperties target, SmHotelShangyouChannelConfigDTO dto) {
|
||||
if (StrUtil.isNotBlank(dto.getBaseUrl())) {
|
||||
target.setBaseUrl(dto.getBaseUrl().trim());
|
||||
}
|
||||
if (dto.getAuthBaseUrl() != null) {
|
||||
target.setAuthBaseUrl(StrUtil.trimToEmpty(dto.getAuthBaseUrl()));
|
||||
}
|
||||
if (dto.getBizBaseUrl() != null) {
|
||||
target.setBizBaseUrl(StrUtil.trimToEmpty(dto.getBizBaseUrl()));
|
||||
}
|
||||
if (StrUtil.isNotBlank(dto.getClientId())) {
|
||||
target.setClientId(dto.getClientId().trim());
|
||||
}
|
||||
if (StrUtil.isNotBlank(dto.getClientSecret())) {
|
||||
target.setClientSecret(dto.getClientSecret().trim());
|
||||
}
|
||||
if (StrUtil.isNotBlank(dto.getCardId())) {
|
||||
target.setCardId(dto.getCardId().trim());
|
||||
}
|
||||
if (StrUtil.isNotBlank(dto.getCardPassword())) {
|
||||
target.setCardPassword(dto.getCardPassword().trim());
|
||||
}
|
||||
if (dto.getCallbackSecret() != null) {
|
||||
target.setCallbackSecret(StrUtil.trimToEmpty(dto.getCallbackSecret()));
|
||||
}
|
||||
if (StrUtil.isNotBlank(dto.getBookerId())) {
|
||||
target.setBookerId(dto.getBookerId().trim());
|
||||
}
|
||||
if (dto.getOrderIsGroup() != null) {
|
||||
target.setOrderIsGroup(dto.getOrderIsGroup());
|
||||
}
|
||||
if (StrUtil.isNotBlank(dto.getOrderGuaranteeMode())) {
|
||||
target.setOrderGuaranteeMode(dto.getOrderGuaranteeMode().trim());
|
||||
}
|
||||
if (StrUtil.isNotBlank(dto.getRoomPricePath())) {
|
||||
target.setRoomPricePath(dto.getRoomPricePath().trim());
|
||||
}
|
||||
if (dto.getMemberLevels() != null) {
|
||||
target.setMemberLevels(StrUtil.trimToEmpty(dto.getMemberLevels()));
|
||||
}
|
||||
if (dto.getTimeoutMs() != null && dto.getTimeoutMs() > 0) {
|
||||
target.setTimeoutMs(dto.getTimeoutMs());
|
||||
}
|
||||
if (dto.getTokenSkewSeconds() != null && dto.getTokenSkewSeconds() >= 0) {
|
||||
target.setTokenSkewSeconds(dto.getTokenSkewSeconds());
|
||||
}
|
||||
if (dto.getPriceDays() != null && dto.getPriceDays() > 0) {
|
||||
target.setPriceDays(dto.getPriceDays());
|
||||
}
|
||||
if (dto.getHotelListCacheMinutes() != null && dto.getHotelListCacheMinutes() > 0) {
|
||||
target.setHotelListCacheMinutes(dto.getHotelListCacheMinutes());
|
||||
}
|
||||
if (dto.getHotelListWarnSize() != null && dto.getHotelListWarnSize() > 0) {
|
||||
target.setHotelListWarnSize(dto.getHotelListWarnSize());
|
||||
}
|
||||
if (dto.getCallbackTenantId() != null) {
|
||||
target.setCallbackTenantId(dto.getCallbackTenantId());
|
||||
}
|
||||
if (dto.getCallbackQueueEnabled() != null) {
|
||||
target.setCallbackQueueEnabled(dto.getCallbackQueueEnabled());
|
||||
}
|
||||
if (dto.getCallbackMaxAttempts() != null && dto.getCallbackMaxAttempts() > 0) {
|
||||
target.setCallbackMaxAttempts(dto.getCallbackMaxAttempts());
|
||||
}
|
||||
if (dto.getCallbackTaskIntervalMs() != null && dto.getCallbackTaskIntervalMs() >= 0) {
|
||||
target.setCallbackTaskIntervalMs(dto.getCallbackTaskIntervalMs());
|
||||
}
|
||||
if (dto.getPriceApiQps() != null) {
|
||||
target.setPriceApiQps(dto.getPriceApiQps());
|
||||
}
|
||||
if (dto.getAppPayTimeoutMinutes() != null && dto.getAppPayTimeoutMinutes() > 0) {
|
||||
target.setAppPayTimeoutMinutes(dto.getAppPayTimeoutMinutes());
|
||||
}
|
||||
if (dto.getSpringScheduleEnabled() != null) {
|
||||
target.setSpringScheduleEnabled(dto.getSpringScheduleEnabled());
|
||||
}
|
||||
if (dto.getHotelListDailySyncEnabled() != null) {
|
||||
target.setHotelListDailySyncEnabled(dto.getHotelListDailySyncEnabled());
|
||||
}
|
||||
if (StrUtil.isNotBlank(dto.getHotelListDailyCron())) {
|
||||
target.setHotelListDailyCron(dto.getHotelListDailyCron().trim());
|
||||
}
|
||||
if (dto.getPriceRollDailyEnabled() != null) {
|
||||
target.setPriceRollDailyEnabled(dto.getPriceRollDailyEnabled());
|
||||
}
|
||||
if (StrUtil.isNotBlank(dto.getPriceRollDailyCron())) {
|
||||
target.setPriceRollDailyCron(dto.getPriceRollDailyCron().trim());
|
||||
}
|
||||
if (dto.getLocalDailyRollEnabled() != null) {
|
||||
target.setLocalDailyRollEnabled(dto.getLocalDailyRollEnabled());
|
||||
}
|
||||
if (StrUtil.isNotBlank(dto.getLocalDailyRollCron())) {
|
||||
target.setLocalDailyRollCron(dto.getLocalDailyRollCron().trim());
|
||||
}
|
||||
if (dto.getAutoImportNewHotels() != null) {
|
||||
target.setAutoImportNewHotels(dto.getAutoImportNewHotels());
|
||||
}
|
||||
if (dto.getAutoImportBatchSize() != null && dto.getAutoImportBatchSize() > 0) {
|
||||
target.setAutoImportBatchSize(dto.getAutoImportBatchSize());
|
||||
}
|
||||
if (dto.getPriceRollDayOffset() != null && dto.getPriceRollDayOffset() > 0) {
|
||||
target.setPriceRollDayOffset(dto.getPriceRollDayOffset());
|
||||
}
|
||||
if (dto.getPriceRollBatchSize() != null && dto.getPriceRollBatchSize() > 0) {
|
||||
target.setPriceRollBatchSize(dto.getPriceRollBatchSize());
|
||||
}
|
||||
if (dto.getPriceRollIntervalMs() != null && dto.getPriceRollIntervalMs() >= 0) {
|
||||
target.setPriceRollIntervalMs(dto.getPriceRollIntervalMs());
|
||||
}
|
||||
if (dto.getFullPriceSyncMaxPerDay() != null && dto.getFullPriceSyncMaxPerDay() > 0) {
|
||||
target.setFullPriceSyncMaxPerDay(dto.getFullPriceSyncMaxPerDay());
|
||||
}
|
||||
if (dto.getFullPriceSyncMinIntervalHours() != null && dto.getFullPriceSyncMinIntervalHours() >= 0) {
|
||||
target.setFullPriceSyncMinIntervalHours(dto.getFullPriceSyncMinIntervalHours());
|
||||
}
|
||||
if (dto.getDetailPullIntervalMs() != null && dto.getDetailPullIntervalMs() >= 0) {
|
||||
target.setDetailPullIntervalMs(dto.getDetailPullIntervalMs());
|
||||
}
|
||||
if (dto.getDetailPullMaxHotels() != null && dto.getDetailPullMaxHotels() >= 0) {
|
||||
target.setDetailPullMaxHotels(dto.getDetailPullMaxHotels());
|
||||
}
|
||||
if (dto.getDiffScanBatchSize() != null && dto.getDiffScanBatchSize() > 0) {
|
||||
target.setDiffScanBatchSize(dto.getDiffScanBatchSize());
|
||||
}
|
||||
if (dto.getAutoMarkOfflineHotels() != null) {
|
||||
target.setAutoMarkOfflineHotels(dto.getAutoMarkOfflineHotels());
|
||||
}
|
||||
if (dto.getAutoReactivateOnlineHotels() != null) {
|
||||
target.setAutoReactivateOnlineHotels(dto.getAutoReactivateOnlineHotels());
|
||||
}
|
||||
if (StrUtil.isNotBlank(dto.getDefaultImportDataSource())) {
|
||||
target.setDefaultImportDataSource(dto.getDefaultImportDataSource().trim());
|
||||
}
|
||||
}
|
||||
|
||||
private static ShangyouHotelProperties copyProps(ShangyouHotelProperties src) {
|
||||
ShangyouHotelProperties copy = new ShangyouHotelProperties();
|
||||
BeanUtil.copyProperties(src, copy);
|
||||
return copy;
|
||||
}
|
||||
|
||||
private static HotelDownstreamProperties copyDownstreamProps(HotelDownstreamProperties src) {
|
||||
HotelDownstreamProperties copy = new HotelDownstreamProperties();
|
||||
BeanUtil.copyProperties(src, copy);
|
||||
return copy;
|
||||
}
|
||||
|
||||
private String normalizeChannelCode(String channel) {
|
||||
return SmHotelDataSource.normalize(channel).toLowerCase();
|
||||
}
|
||||
|
||||
private R<String> validateConfigText(String text) {
|
||||
try {
|
||||
ChannelConfigTextParser.validate(text);
|
||||
return null;
|
||||
}
|
||||
catch (Exception e) {
|
||||
return R.failed("对接参数格式错误(支持 YAML/# 注释 或 JSON):" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
+374
@@ -0,0 +1,374 @@
|
||||
package com.bsd.pjcloud.supermarket.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bsd.pjcloud.admin.api.entity.SysDept;
|
||||
import com.bsd.pjcloud.admin.api.feign.RemoteDeptService;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.supermarket.constant.SmHotelChannelDirection;
|
||||
import com.bsd.pjcloud.supermarket.constant.SmHotelDataSource;
|
||||
import com.bsd.pjcloud.supermarket.constant.SmHotelDownstreamScopeType;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelChannelDeptEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelEntity;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelChannelDeptMapper;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelMapper;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelDeptService;
|
||||
import com.bsd.pjcloud.supermarket.util.HotelAreaAdcodeUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SmHotelChannelDeptServiceImpl extends ServiceImpl<SmHotelChannelDeptMapper, SmHotelChannelDeptEntity>
|
||||
implements SmHotelChannelDeptService {
|
||||
|
||||
private final RemoteDeptService remoteDeptService;
|
||||
|
||||
private final SmHotelMapper hotelMapper;
|
||||
|
||||
@Override
|
||||
public Page<SmHotelChannelDeptEntity> pageMappings(Page<SmHotelChannelDeptEntity> page,
|
||||
SmHotelChannelDeptEntity query) {
|
||||
String dir = StrUtil.isBlank(query.getDirection()) ? null : SmHotelChannelDirection.normalize(query.getDirection());
|
||||
Page<SmHotelChannelDeptEntity> result = baseMapper.selectPage(page, Wrappers.<SmHotelChannelDeptEntity>lambdaQuery()
|
||||
.eq(StrUtil.isNotBlank(query.getChannel()), SmHotelChannelDeptEntity::getChannel,
|
||||
normalizeCode(query.getChannel()))
|
||||
.eq(dir != null, SmHotelChannelDeptEntity::getDirection, dir)
|
||||
.eq(StrUtil.isNotBlank(query.getScopeType()), SmHotelChannelDeptEntity::getScopeType,
|
||||
SmHotelDownstreamScopeType.normalize(query.getScopeType()))
|
||||
.eq(query.getDeptId() != null, SmHotelChannelDeptEntity::getDeptId, query.getDeptId())
|
||||
.eq(StrUtil.isNotBlank(query.getStatus()), SmHotelChannelDeptEntity::getStatus, query.getStatus())
|
||||
.orderByDesc(SmHotelChannelDeptEntity::getCreateTime));
|
||||
fillDeptNames(result.getRecords());
|
||||
fillHotelNames(result.getRecords());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SmHotelChannelDeptEntity> listEnabled() {
|
||||
List<SmHotelChannelDeptEntity> list = baseMapper.selectList(Wrappers.<SmHotelChannelDeptEntity>lambdaQuery()
|
||||
.eq(SmHotelChannelDeptEntity::getStatus, "1")
|
||||
.orderByAsc(SmHotelChannelDeptEntity::getDirection)
|
||||
.orderByAsc(SmHotelChannelDeptEntity::getChannel));
|
||||
fillDeptNames(list);
|
||||
fillHotelNames(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<Boolean> saveMapping(SmHotelChannelDeptEntity entity) {
|
||||
R<String> valid = validate(entity, null);
|
||||
if (valid != null) {
|
||||
return R.failed(valid.getMsg());
|
||||
}
|
||||
normalizeEntity(entity);
|
||||
if (StrUtil.isBlank(entity.getStatus())) {
|
||||
entity.setStatus("1");
|
||||
}
|
||||
return R.ok(baseMapper.insert(entity) > 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<Boolean> updateMapping(SmHotelChannelDeptEntity entity) {
|
||||
if (entity == null || entity.getId() == null) {
|
||||
return R.failed("缺少主键");
|
||||
}
|
||||
SmHotelChannelDeptEntity old = baseMapper.selectById(entity.getId());
|
||||
if (old == null) {
|
||||
return R.failed("记录不存在");
|
||||
}
|
||||
R<String> valid = validate(entity, entity.getId());
|
||||
if (valid != null) {
|
||||
return R.failed(valid.getMsg());
|
||||
}
|
||||
normalizeEntity(entity);
|
||||
return R.ok(baseMapper.updateById(entity) > 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<Boolean> removeMappings(Long[] ids) {
|
||||
if (ids == null || ids.length == 0) {
|
||||
return R.failed("请选择要删除的记录");
|
||||
}
|
||||
return R.ok(removeByIds(Arrays.asList(ids)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long resolveDeptId(String channel) {
|
||||
String ch = normalizeCode(channel);
|
||||
if (ch.isEmpty() || SmHotelDataSource.isLocal(ch)) {
|
||||
return null;
|
||||
}
|
||||
SmHotelChannelDeptEntity row = baseMapper.selectOne(Wrappers.<SmHotelChannelDeptEntity>lambdaQuery()
|
||||
.eq(SmHotelChannelDeptEntity::getChannel, ch)
|
||||
.eq(SmHotelChannelDeptEntity::getDirection, SmHotelChannelDirection.UPSTREAM)
|
||||
.eq(SmHotelChannelDeptEntity::getStatus, "1")
|
||||
.last("limit 1"));
|
||||
return row == null ? null : row.getDeptId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<Long> requireEnabledDeptId(String channel) {
|
||||
String ch = SmHotelDataSource.normalize(channel);
|
||||
if (StrUtil.isBlank(ch)) {
|
||||
return R.failed("请先选择第三方渠道");
|
||||
}
|
||||
if (SmHotelDataSource.isLocal(ch)) {
|
||||
return R.failed("不能选择「本地」渠道,请先在「渠道部门」维护第三方对应关系");
|
||||
}
|
||||
Long deptId = resolveDeptId(ch);
|
||||
if (deptId == null) {
|
||||
return R.failed("渠道「" + ch + "」尚未维护归属部门,请先到「渠道部门」配置并启用");
|
||||
}
|
||||
return R.ok(deptId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> listDownstreamPartnersForHotel(Long hotelId, Long deptId, Long districtAdcode, String district) {
|
||||
List<SmHotelChannelDeptEntity> rows = baseMapper.selectList(Wrappers.<SmHotelChannelDeptEntity>lambdaQuery()
|
||||
.eq(SmHotelChannelDeptEntity::getDirection, SmHotelChannelDirection.DOWNSTREAM)
|
||||
.eq(SmHotelChannelDeptEntity::getStatus, "1"));
|
||||
if (CollUtil.isEmpty(rows)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Set<String> partners = new LinkedHashSet<>();
|
||||
for (SmHotelChannelDeptEntity row : rows) {
|
||||
String scope = SmHotelDownstreamScopeType.normalize(row.getScopeType());
|
||||
if (SmHotelDownstreamScopeType.isHotel(scope)) {
|
||||
if (hotelId != null && containsHotelId(row.getHotelIds(), hotelId)) {
|
||||
partners.add(normalizeCode(row.getChannel()));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// 按部门
|
||||
if (deptId == null || row.getDeptId() == null || !deptId.equals(row.getDeptId())) {
|
||||
continue;
|
||||
}
|
||||
if (HotelAreaAdcodeUtil.matchRegion(row.getDistrictAdcode(), row.getDistrict(), districtAdcode, district)) {
|
||||
partners.add(normalizeCode(row.getChannel()));
|
||||
}
|
||||
}
|
||||
return partners.stream().filter(StrUtil::isNotBlank).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private void normalizeEntity(SmHotelChannelDeptEntity entity) {
|
||||
entity.setChannel(normalizeCode(entity.getChannel()));
|
||||
entity.setDirection(SmHotelChannelDirection.normalize(entity.getDirection()));
|
||||
if (SmHotelChannelDirection.isUpstream(entity.getDirection())) {
|
||||
entity.setScopeType(SmHotelDownstreamScopeType.DEPT);
|
||||
entity.setHotelIds(null);
|
||||
entity.setDistrictAdcode(null);
|
||||
entity.setDistrict(null);
|
||||
return;
|
||||
}
|
||||
String scope = SmHotelDownstreamScopeType.normalize(entity.getScopeType());
|
||||
entity.setScopeType(scope);
|
||||
if (SmHotelDownstreamScopeType.isHotel(scope)) {
|
||||
entity.setDeptId(null);
|
||||
entity.setDistrictAdcode(null);
|
||||
entity.setDistrict(null);
|
||||
entity.setHotelIds(normalizeHotelIds(entity.getHotelIds()));
|
||||
}
|
||||
else {
|
||||
entity.setHotelIds(null);
|
||||
}
|
||||
}
|
||||
|
||||
private R<String> validate(SmHotelChannelDeptEntity entity, Long excludeId) {
|
||||
if (entity == null) {
|
||||
return R.failed("参数不能为空");
|
||||
}
|
||||
String dir = SmHotelChannelDirection.normalize(entity.getDirection());
|
||||
String ch = SmHotelDataSource.normalize(entity.getChannel());
|
||||
if (StrUtil.isBlank(ch)) {
|
||||
return R.failed(SmHotelChannelDirection.isDownstream(dir) ? "请选择下游伙伴" : "请选择第三方渠道");
|
||||
}
|
||||
if (SmHotelChannelDirection.isUpstream(dir) && SmHotelDataSource.isLocal(ch)) {
|
||||
return R.failed("不能将「本地」配置为第三方渠道");
|
||||
}
|
||||
if (SmHotelChannelDirection.isUpstream(dir)) {
|
||||
if (entity.getDeptId() == null) {
|
||||
return R.failed("请选择归属部门");
|
||||
}
|
||||
Long cnt = baseMapper.selectCount(Wrappers.<SmHotelChannelDeptEntity>lambdaQuery()
|
||||
.eq(SmHotelChannelDeptEntity::getChannel, ch.toLowerCase())
|
||||
.eq(SmHotelChannelDeptEntity::getDirection, SmHotelChannelDirection.UPSTREAM)
|
||||
.ne(excludeId != null, SmHotelChannelDeptEntity::getId, excludeId));
|
||||
if (cnt != null && cnt > 0) {
|
||||
return R.failed("该上游渠道已配置归属部门,请直接编辑");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String scope = SmHotelDownstreamScopeType.normalize(entity.getScopeType());
|
||||
if (SmHotelDownstreamScopeType.isHotel(scope)) {
|
||||
String ids = normalizeHotelIds(entity.getHotelIds());
|
||||
if (StrUtil.isBlank(ids)) {
|
||||
return R.failed("请选择要授权的酒店");
|
||||
}
|
||||
entity.setHotelIds(ids);
|
||||
Long cnt = baseMapper.selectCount(Wrappers.<SmHotelChannelDeptEntity>lambdaQuery()
|
||||
.eq(SmHotelChannelDeptEntity::getChannel, ch.toLowerCase())
|
||||
.eq(SmHotelChannelDeptEntity::getDirection, SmHotelChannelDirection.DOWNSTREAM)
|
||||
.eq(SmHotelChannelDeptEntity::getScopeType, SmHotelDownstreamScopeType.HOTEL)
|
||||
.ne(excludeId != null, SmHotelChannelDeptEntity::getId, excludeId));
|
||||
if (cnt != null && cnt > 0) {
|
||||
return R.failed("该下游伙伴已有「按指定酒店」配置,请直接编辑");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (entity.getDeptId() == null) {
|
||||
return R.failed("请选择要推送的部门");
|
||||
}
|
||||
Long cnt = baseMapper.selectCount(Wrappers.<SmHotelChannelDeptEntity>lambdaQuery()
|
||||
.eq(SmHotelChannelDeptEntity::getChannel, ch.toLowerCase())
|
||||
.eq(SmHotelChannelDeptEntity::getDirection, SmHotelChannelDirection.DOWNSTREAM)
|
||||
.and(w -> w.eq(SmHotelChannelDeptEntity::getScopeType, SmHotelDownstreamScopeType.DEPT)
|
||||
.or()
|
||||
.isNull(SmHotelChannelDeptEntity::getScopeType)
|
||||
.or()
|
||||
.eq(SmHotelChannelDeptEntity::getScopeType, ""))
|
||||
.eq(SmHotelChannelDeptEntity::getDeptId, entity.getDeptId())
|
||||
.and(w -> {
|
||||
if (entity.getDistrictAdcode() == null) {
|
||||
w.isNull(SmHotelChannelDeptEntity::getDistrictAdcode);
|
||||
}
|
||||
else {
|
||||
w.eq(SmHotelChannelDeptEntity::getDistrictAdcode, entity.getDistrictAdcode());
|
||||
}
|
||||
})
|
||||
.ne(excludeId != null, SmHotelChannelDeptEntity::getId, excludeId));
|
||||
if (cnt != null && cnt > 0) {
|
||||
return R.failed(entity.getDistrictAdcode() == null
|
||||
? "该下游伙伴已配置此部门(全部区域),请勿重复"
|
||||
: "该下游伙伴已配置此部门+区域,请勿重复");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String normalizeHotelIds(String raw) {
|
||||
if (StrUtil.isBlank(raw)) {
|
||||
return null;
|
||||
}
|
||||
Set<String> ids = new LinkedHashSet<>();
|
||||
for (String part : raw.split("[,,\\s]+")) {
|
||||
String p = StrUtil.trim(part);
|
||||
if (StrUtil.isNotBlank(p) && p.matches("\\d+")) {
|
||||
ids.add(p);
|
||||
}
|
||||
}
|
||||
return ids.isEmpty() ? null : String.join(",", ids);
|
||||
}
|
||||
|
||||
private static boolean containsHotelId(String hotelIds, Long hotelId) {
|
||||
if (hotelId == null || StrUtil.isBlank(hotelIds)) {
|
||||
return false;
|
||||
}
|
||||
String target = String.valueOf(hotelId);
|
||||
for (String part : hotelIds.split(",")) {
|
||||
if (target.equals(StrUtil.trim(part))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String normalizeCode(String channel) {
|
||||
return SmHotelDataSource.normalize(channel).toLowerCase();
|
||||
}
|
||||
|
||||
private void fillDeptNames(List<SmHotelChannelDeptEntity> records) {
|
||||
if (records == null || records.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<Long> deptIds = records.stream().map(SmHotelChannelDeptEntity::getDeptId).filter(Objects::nonNull)
|
||||
.distinct().collect(Collectors.toList());
|
||||
Map<Long, String> nameById = new HashMap<>();
|
||||
if (!deptIds.isEmpty()) {
|
||||
try {
|
||||
R<Map<Long, SysDept>> r = remoteDeptService.getDeptByDeptIds(deptIds);
|
||||
if (r != null && r.isOk() && r.getData() != null) {
|
||||
r.getData().forEach((id, d) -> {
|
||||
if (id != null && d != null) {
|
||||
nameById.put(id, d.getName());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|渠道部门|补全部门名失败|{}", e.getMessage());
|
||||
}
|
||||
}
|
||||
for (SmHotelChannelDeptEntity row : records) {
|
||||
if (row.getDeptId() != null) {
|
||||
row.setDeptName(nameById.getOrDefault(row.getDeptId(), String.valueOf(row.getDeptId())));
|
||||
}
|
||||
row.setChannelLabel(row.getChannel());
|
||||
if (StrUtil.isBlank(row.getDirection())) {
|
||||
row.setDirection(SmHotelChannelDirection.UPSTREAM);
|
||||
}
|
||||
if (StrUtil.isBlank(row.getScopeType())) {
|
||||
row.setScopeType(SmHotelDownstreamScopeType.DEPT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void fillHotelNames(List<SmHotelChannelDeptEntity> records) {
|
||||
if (records == null || records.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<Long> allIds = new HashSet<>();
|
||||
for (SmHotelChannelDeptEntity row : records) {
|
||||
if (!SmHotelDownstreamScopeType.isHotel(row.getScopeType()) || StrUtil.isBlank(row.getHotelIds())) {
|
||||
continue;
|
||||
}
|
||||
for (String part : row.getHotelIds().split(",")) {
|
||||
String p = StrUtil.trim(part);
|
||||
if (p.matches("\\d+")) {
|
||||
allIds.add(Long.valueOf(p));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (allIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<SmHotelEntity> hotels = hotelMapper.selectByIds(allIds);
|
||||
Map<Long, String> nameById = hotels.stream()
|
||||
.filter(h -> h != null && h.getId() != null)
|
||||
.collect(Collectors.toMap(SmHotelEntity::getId,
|
||||
h -> StrUtil.blankToDefault(h.getHotelName(), String.valueOf(h.getId())), (a, b) -> a));
|
||||
for (SmHotelChannelDeptEntity row : records) {
|
||||
if (!SmHotelDownstreamScopeType.isHotel(row.getScopeType()) || StrUtil.isBlank(row.getHotelIds())) {
|
||||
continue;
|
||||
}
|
||||
List<String> names = Arrays.stream(row.getHotelIds().split(","))
|
||||
.map(StrUtil::trim)
|
||||
.filter(p -> p.matches("\\d+"))
|
||||
.map(Long::valueOf)
|
||||
.map(id -> nameById.getOrDefault(id, String.valueOf(id)))
|
||||
.collect(Collectors.toList());
|
||||
row.setHotelNames(String.join("、", names));
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.bsd.pjcloud.supermarket.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelDownstreamPushLogEntity;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelDownstreamPushLogMapper;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelDownstreamPushLogService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SmHotelDownstreamPushLogServiceImpl
|
||||
extends ServiceImpl<SmHotelDownstreamPushLogMapper, SmHotelDownstreamPushLogEntity>
|
||||
implements SmHotelDownstreamPushLogService {
|
||||
|
||||
@Override
|
||||
public Page<SmHotelDownstreamPushLogEntity> pageLogs(Page<SmHotelDownstreamPushLogEntity> page,
|
||||
SmHotelDownstreamPushLogEntity query) {
|
||||
return baseMapper.selectPage(page, Wrappers.<SmHotelDownstreamPushLogEntity>lambdaQuery()
|
||||
.eq(StrUtil.isNotBlank(query.getPartnerCode()), SmHotelDownstreamPushLogEntity::getPartnerCode,
|
||||
query.getPartnerCode())
|
||||
.eq(query.getHotelId() != null, SmHotelDownstreamPushLogEntity::getHotelId, query.getHotelId())
|
||||
.eq(StrUtil.isNotBlank(query.getExtHotelId()), SmHotelDownstreamPushLogEntity::getExtHotelId,
|
||||
query.getExtHotelId())
|
||||
.eq(StrUtil.isNotBlank(query.getAction()), SmHotelDownstreamPushLogEntity::getAction, query.getAction())
|
||||
.eq(StrUtil.isNotBlank(query.getSuccess()), SmHotelDownstreamPushLogEntity::getSuccess, query.getSuccess())
|
||||
.orderByDesc(SmHotelDownstreamPushLogEntity::getCreateTime));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void record(SmHotelDownstreamPushLogEntity logEntity) {
|
||||
if (logEntity == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
baseMapper.insert(logEntity);
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|下游推送流水|落库失败|partner={}|action={}|err={}", logEntity.getPartnerCode(),
|
||||
logEntity.getAction(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
+164
-26
@@ -17,6 +17,8 @@ import com.bsd.pjcloud.supermarket.integration.downstream.DownstreamHotelClient;
|
||||
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.SmHotelChannelConfigService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelDeptService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelMidService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelPriceConfigService;
|
||||
import com.bsd.pjcloud.supermarket.vo.SmHotelDetailVO;
|
||||
@@ -33,6 +35,7 @@ import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 中转核心:上游(玉编电云)拉取 → 加价落库 → 推下游。
|
||||
@@ -61,6 +64,10 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
|
||||
private final SmHotelPriceConfigService priceConfigService;
|
||||
|
||||
private final SmHotelChannelConfigService channelConfigService;
|
||||
|
||||
private final SmHotelChannelDeptService channelDeptService;
|
||||
|
||||
private final SmHotelMapper hotelMapper;
|
||||
|
||||
private final SmHotelRoomMapper roomMapper;
|
||||
@@ -322,24 +329,104 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
@Override
|
||||
public R<Map<String, Object>> pushToDownstream(Long hotelId) {
|
||||
SmHotelEntity hotel = requireHotel(hotelId);
|
||||
List<String> partners = resolvePushPartners(hotel);
|
||||
if (CollUtil.isEmpty(partners)) {
|
||||
return R.failed("该酒店未命中下游推送范围(请在「渠道范围映射」按部门+位置区域配置下游,并启用对接配置)");
|
||||
}
|
||||
List<Map<String, Object>> partnerResults = new ArrayList<>();
|
||||
List<String> partnerErrors = new ArrayList<>();
|
||||
for (String partner : partners) {
|
||||
try {
|
||||
Map<String, Object> one = pushHotelToPartner(hotel, partner);
|
||||
one.put("partnerCode", partner);
|
||||
partnerResults.add(one);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
partnerErrors.add(partner + ":" + ex.getMessage());
|
||||
log.warn("|下游推送失败|hotelId={}|partner={}|err={}", hotelId, partner, ex.getMessage());
|
||||
}
|
||||
}
|
||||
Map<String, Object> result = new HashMap<>(8);
|
||||
result.put("partners", partners);
|
||||
result.put("partnerResults", partnerResults);
|
||||
result.put("partnerErrors", partnerErrors);
|
||||
if (partnerResults.isEmpty()) {
|
||||
return R.failed("全部下游推送失败:" + String.join("; ", partnerErrors));
|
||||
}
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按酒店部门 + 位置区域匹配下游伙伴:范围映射 ∩ 已启用对接配置。
|
||||
* 若无命中映射但存在「default」YAML 兼容下游,则仍推送 default。
|
||||
*/
|
||||
private List<String> resolvePushPartners(SmHotelEntity hotel) {
|
||||
List<String> enabled = channelConfigService.listEnabledDownstreamPartners();
|
||||
if (CollUtil.isEmpty(enabled)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<String> matched = channelDeptService.listDownstreamPartnersForHotel(hotel.getId(), hotel.getDeptId(),
|
||||
hotel.getDistrictAdcode(), hotel.getDistrict());
|
||||
if (CollUtil.isEmpty(matched)) {
|
||||
// 兼容:仅配置了 YAML 默认下游、未做范围映射时推 default
|
||||
if (enabled.contains("default")) {
|
||||
return List.of("default");
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Set<String> enabledSet = new HashSet<>(enabled);
|
||||
return matched.stream().filter(enabledSet::contains).distinct().toList();
|
||||
}
|
||||
|
||||
/** 按下游协议约定顺序推送:酒店→房型→产品→日历 */
|
||||
private Map<String, Object> pushHotelToPartner(SmHotelEntity hotel, String partnerCode) {
|
||||
Long hotelId = hotel.getId();
|
||||
String extHotelId = StrUtil.blankToDefault(hotel.getHotelCode(), String.valueOf(hotel.getId()));
|
||||
downstreamClient.assertPartnerReady(partnerCode);
|
||||
|
||||
Map<String, Object> hotelBody = new LinkedHashMap<>();
|
||||
hotelBody.put("extHotelId", extHotelId);
|
||||
hotelBody.put("hotelName", StrUtil.blankToDefault(hotel.getHotelName(), extHotelId));
|
||||
if (StrUtil.isNotBlank(hotel.getDistrict())) {
|
||||
hotelBody.put("cityName", hotel.getDistrict());
|
||||
}
|
||||
if (StrUtil.isNotBlank(hotel.getAddress())) {
|
||||
hotelBody.put("address", hotel.getAddress());
|
||||
}
|
||||
if (StrUtil.isNotBlank(hotel.getPhone())) {
|
||||
hotelBody.put("hotelPhone", hotel.getPhone());
|
||||
}
|
||||
parseRemarkAddress(hotel.getRemark(), hotelBody);
|
||||
downstreamClient.hotelSync(hotelBody);
|
||||
// 环启必填前台电话 receptionPhone;我方无独立前台号,复用 hotelPhone
|
||||
Object phoneObj = hotelBody.get("hotelPhone");
|
||||
String phone = phoneObj != null ? StrUtil.trim(String.valueOf(phoneObj)) : null;
|
||||
if (StrUtil.isBlank(phone)) {
|
||||
phone = StrUtil.trim(hotel.getPhone());
|
||||
}
|
||||
if (StrUtil.isNotBlank(phone)) {
|
||||
hotelBody.put("hotelPhone", phone);
|
||||
hotelBody.put("receptionPhone", phone);
|
||||
}
|
||||
downstreamClient.hotelSync(partnerCode, hotelId, extHotelId, hotelBody);
|
||||
|
||||
List<SmHotelRoomEntity> rooms = roomMapper.selectList(Wrappers.<SmHotelRoomEntity>lambdaQuery()
|
||||
List<SmHotelRoomEntity> allRooms = roomMapper.selectList(Wrappers.<SmHotelRoomEntity>lambdaQuery()
|
||||
.eq(SmHotelRoomEntity::getHotelId, hotelId));
|
||||
List<SmHotelProductEntity> products = productMapper
|
||||
List<SmHotelProductEntity> allProducts = productMapper
|
||||
.selectList(Wrappers.<SmHotelProductEntity>lambdaQuery()
|
||||
.eq(SmHotelProductEntity::getHotelId, hotelId));
|
||||
// 可住人数为 0 的房型不推下游(空容量仍按 2 推)
|
||||
List<SmHotelRoomEntity> rooms = allRooms.stream()
|
||||
.filter(r -> r.getCapacity() == null || r.getCapacity() > 0)
|
||||
.collect(Collectors.toList());
|
||||
Set<String> pushedRoomCodes = rooms.stream()
|
||||
.map(SmHotelRoomEntity::getRoomTypeCode)
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.collect(Collectors.toCollection(HashSet::new));
|
||||
Map<String, Integer> roomQtyMap = new HashMap<>();
|
||||
for (SmHotelRoomEntity room : rooms) {
|
||||
int qty = room.getRoomNumber() == null ? 0 : room.getRoomNumber();
|
||||
if (qty <= 0) {
|
||||
qty = products.stream()
|
||||
qty = allProducts.stream()
|
||||
.filter(p -> StrUtil.equals(room.getRoomTypeCode(), p.getRoomTypeCode()))
|
||||
.map(p -> p.getStock() == null ? 0 : p.getStock())
|
||||
.max(Integer::compareTo)
|
||||
@@ -357,7 +444,8 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
item.put("extApartmentId", room.getRoomTypeCode());
|
||||
item.put("apartmentName", StrUtil.blankToDefault(room.getRoomTypeName(), room.getRoomTypeCode()));
|
||||
item.put("bedType", room.getBedType());
|
||||
item.put("capacity", room.getCapacity());
|
||||
int capacity = room.getCapacity() == null ? 2 : room.getCapacity();
|
||||
item.put("capacity", capacity);
|
||||
item.put("apartmentNumber", roomQtyMap.getOrDefault(room.getRoomTypeCode(), 1));
|
||||
list.add(item);
|
||||
}
|
||||
@@ -365,23 +453,43 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("extHotelId", extHotelId);
|
||||
body.put("list", batch);
|
||||
downstreamClient.apartmentSync(body);
|
||||
downstreamClient.apartmentSync(partnerCode, hotelId, extHotelId, body);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 产品:不看取消规则(本地无可靠取消数据,环启 canCancel=0 时也不必填)。
|
||||
* 同房型+同早餐只能推 1 条:在有库存的候选里取价格最低的。
|
||||
*/
|
||||
List<String> productErrors = new ArrayList<>();
|
||||
int productOk = 0;
|
||||
Set<String> pricePlanKeys = new HashSet<>();
|
||||
for (SmHotelProductEntity p : products) {
|
||||
int productSkipped = 0;
|
||||
Set<String> pushedProductCodes = new HashSet<>();
|
||||
Map<String, List<SmHotelProductEntity>> productGroups = new LinkedHashMap<>();
|
||||
for (SmHotelProductEntity p : allProducts) {
|
||||
String roomCode = StrUtil.blankToDefault(p.getRoomTypeCode(), "_");
|
||||
int breakfast = p.getMealCount() == null ? 0 : p.getMealCount();
|
||||
int canCancel = p.getCanCancel() == null ? 0 : p.getCanCancel();
|
||||
String cancelRule = StrUtil.nullToEmpty(p.getCancelRule());
|
||||
String planKey = roomCode + "|" + breakfast + "|" + canCancel + "|" + cancelRule;
|
||||
if (!pricePlanKeys.add(planKey)) {
|
||||
productErrors.add(p.getProductCode() + ":跳过,同房型下早餐/取消规则与已推产品重复");
|
||||
if (!pushedRoomCodes.contains(roomCode)) {
|
||||
productSkipped++;
|
||||
continue;
|
||||
}
|
||||
int breakfast = p.getMealCount() == null ? 0 : p.getMealCount();
|
||||
String planKey = roomCode + "|" + breakfast;
|
||||
productGroups.computeIfAbsent(planKey, k -> new ArrayList<>()).add(p);
|
||||
}
|
||||
for (Map.Entry<String, List<SmHotelProductEntity>> e : productGroups.entrySet()) {
|
||||
List<SmHotelProductEntity> group = e.getValue();
|
||||
List<SmHotelProductEntity> inStock = group.stream()
|
||||
.filter(p -> p.getStock() != null && p.getStock() > 0)
|
||||
.collect(Collectors.toList());
|
||||
if (inStock.isEmpty()) {
|
||||
productSkipped += group.size();
|
||||
productErrors.add(e.getKey() + ":同房型+早餐均无库存,整组跳过");
|
||||
continue;
|
||||
}
|
||||
SmHotelProductEntity p = inProductLowestPrice(inStock);
|
||||
productSkipped += group.size() - 1;
|
||||
String roomCode = StrUtil.blankToDefault(p.getRoomTypeCode(), "_");
|
||||
int breakfast = p.getMealCount() == null ? 0 : p.getMealCount();
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("extHotelId", extHotelId);
|
||||
body.put("extApartmentId", roomCode);
|
||||
@@ -389,21 +497,20 @@ 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());
|
||||
body.put("canCancel", canCancel);
|
||||
if (StrUtil.isNotBlank(p.getCancelRule())) {
|
||||
body.put("cancelRule", p.getCancelRule());
|
||||
}
|
||||
// 不传取消规则,固定不可取消,避免环启要求 cancelRule
|
||||
body.put("canCancel", 0);
|
||||
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));
|
||||
try {
|
||||
downstreamClient.productSync(body);
|
||||
downstreamClient.productSync(partnerCode, hotelId, extHotelId, body);
|
||||
productOk++;
|
||||
pushedProductCodes.add(p.getProductCode());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
productErrors.add(p.getProductCode() + ":" + ex.getMessage());
|
||||
log.warn("|下游产品推送失败|hotelId={}|product={}|err={}", hotelId, p.getProductCode(),
|
||||
ex.getMessage());
|
||||
log.warn("|下游产品推送失败|hotelId={}|partner={}|product={}|err={}", hotelId, partnerCode,
|
||||
p.getProductCode(), ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,6 +521,10 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
int dailyPush = 0;
|
||||
List<Map<String, Object>> dailyBatch = new ArrayList<>();
|
||||
for (SmHotelDailyEntity d : dailies) {
|
||||
if (!pushedRoomCodes.contains(d.getRoomTypeCode())
|
||||
|| !pushedProductCodes.contains(d.getProductCode())) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("extApartmentId", d.getRoomTypeCode());
|
||||
item.put("extProductId", d.getProductCode());
|
||||
@@ -427,7 +538,7 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("extHotelId", extHotelId);
|
||||
body.put("list", dailyBatch);
|
||||
downstreamClient.dailySync(body);
|
||||
downstreamClient.dailySync(partnerCode, hotelId, extHotelId, body);
|
||||
dailyPush += dailyBatch.size();
|
||||
dailyBatch = new ArrayList<>();
|
||||
}
|
||||
@@ -436,17 +547,37 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("extHotelId", extHotelId);
|
||||
body.put("list", dailyBatch);
|
||||
downstreamClient.dailySync(body);
|
||||
downstreamClient.dailySync(partnerCode, hotelId, extHotelId, body);
|
||||
dailyPush += dailyBatch.size();
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>(8);
|
||||
result.put("roomCount", rooms.size());
|
||||
result.put("productCount", products.size());
|
||||
result.put("roomSkippedCapacity0", allRooms.size() - rooms.size());
|
||||
result.put("productCount", allProducts.size());
|
||||
result.put("productPushOk", productOk);
|
||||
result.put("productSkipped", productSkipped);
|
||||
result.put("productErrors", productErrors);
|
||||
result.put("dailyPushCount", dailyPush);
|
||||
return R.ok(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 有库存候选里取卖价(无则成本价)最低的一条 */
|
||||
private static SmHotelProductEntity inProductLowestPrice(List<SmHotelProductEntity> inStock) {
|
||||
return inStock.stream().min((a, b) -> {
|
||||
BigDecimal pa = a.getSellPrice() != null ? a.getSellPrice() : a.getCostPrice();
|
||||
BigDecimal pb = b.getSellPrice() != null ? b.getSellPrice() : b.getCostPrice();
|
||||
if (pa == null && pb == null) {
|
||||
return 0;
|
||||
}
|
||||
if (pa == null) {
|
||||
return 1;
|
||||
}
|
||||
if (pb == null) {
|
||||
return -1;
|
||||
}
|
||||
return pa.compareTo(pb);
|
||||
}).orElse(inStock.get(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -641,6 +772,11 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
if (SmHotelDataSource.isLocal(source)) {
|
||||
return R.failed("上游同步不能使用「本地」数据来源,请选择上游类来源");
|
||||
}
|
||||
R<Long> deptR = channelDeptService.requireEnabledDeptId(source);
|
||||
if (deptR == null || !deptR.isOk() || deptR.getData() == null) {
|
||||
return R.failed(deptR == null ? "渠道部门未配置" : deptR.getMsg());
|
||||
}
|
||||
Long deptId = deptR.getData();
|
||||
int insert = 0;
|
||||
int skip = 0;
|
||||
for (Map<String, Object> row : hotels) {
|
||||
@@ -659,7 +795,8 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
SmHotelEntity hotel = new SmHotelEntity();
|
||||
hotel.setHotelCode(hotelId);
|
||||
hotel.setHotelName(StrUtil.blankToDefault(hotelName, hotelId));
|
||||
hotel.setSheetName(source);
|
||||
hotel.setSheetName(SmHotelDataSource.normalize(source).toLowerCase());
|
||||
hotel.setDeptId(deptId);
|
||||
hotel.setUpstreamStatus("ACTIVE");
|
||||
hotelMapper.insert(hotel);
|
||||
stringRedisTemplate.opsForSet().add(ShangyouHotelApi.REDIS_LOCAL_CODE_SET, hotelId);
|
||||
@@ -669,6 +806,7 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
result.put("insertCount", insert);
|
||||
result.put("skipCount", skip);
|
||||
result.put("dataSource", source);
|
||||
result.put("deptId", deptId);
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
|
||||
+247
-67
@@ -10,11 +10,15 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bsd.pjcloud.supermarket.constant.SmHotelDataSource;
|
||||
import com.bsd.pjcloud.supermarket.constant.SmHotelInventoryStatus;
|
||||
import com.bsd.pjcloud.supermarket.entity.*;
|
||||
import com.bsd.pjcloud.supermarket.event.HotelUpstreamFailRefundEvent;
|
||||
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.integration.shangyou.ShangyouHotelClient;
|
||||
import com.bsd.pjcloud.supermarket.integration.shangyou.ShangyouOrderCallResult;
|
||||
import com.bsd.pjcloud.supermarket.mapper.*;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelConfigService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelOrderService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelOrderUpstreamLogService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelPriceConfigService;
|
||||
import com.bsd.pjcloud.supermarket.vo.SmHotelOrderDetailVo;
|
||||
import com.bsd.pjcloud.supermarket.vo.SmHotelOrderPayInfoVo;
|
||||
@@ -27,6 +31,7 @@ import com.bsd.pjcloud.thirdparty.constant.pay.ThirdPayOrderBizTypeEnum;
|
||||
import com.bsd.pjcloud.thirdparty.dto.appplus.ThirdRemoterRefundDto;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -40,6 +45,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -79,11 +85,15 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
|
||||
private final SmHotelDailyMapper dailyMapper;
|
||||
|
||||
private final ShangyouHotelClient upstreamClient;
|
||||
private final HotelUpstreamOrderGatewayRegistry upstreamGatewayRegistry;
|
||||
|
||||
private final SmHotelOrderUpstreamLogService upstreamLogService;
|
||||
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
private final SmHotelPriceConfigService priceConfigService;
|
||||
|
||||
private final com.bsd.pjcloud.supermarket.config.ShangyouHotelProperties shangyouProperties;
|
||||
private final SmHotelChannelConfigService channelConfigService;
|
||||
|
||||
private final RemoterThirdAppPlusPayService remoterThirdAppPlusPayService;
|
||||
|
||||
@@ -184,7 +194,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
}
|
||||
if (hotel != null) {
|
||||
o.setHotelName(hotel.getHotelName());
|
||||
o.setHotelDataSource(SmHotelDataSource.isLocal(hotel.getSheetName()) ? "local" : "shangyou");
|
||||
o.setHotelDataSource(upstreamGatewayRegistry.resolveChannelKey(hotel.getSheetName()));
|
||||
}
|
||||
String channelNo = o.getChannelNo();
|
||||
if (StrUtil.startWithIgnoreCase(channelNo, "APP")) {
|
||||
@@ -224,6 +234,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
vo.setGuests(parseJsonLenient(order.getGuestJson()));
|
||||
vo.setReqSnapshot(parseJsonLenient(order.getReqJson()));
|
||||
vo.setUpstreamResp(parseJsonLenient(order.getUpstreamRespJson()));
|
||||
vo.setUpstreamLogs(upstreamLogService.listByOrderId(order.getId()));
|
||||
|
||||
SmHotelEntity hotel = null;
|
||||
if (order.getHotelId() != null) {
|
||||
@@ -240,7 +251,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
hb.setHotelCode(hotel.getHotelCode());
|
||||
hb.setHotelName(hotel.getHotelName());
|
||||
hb.setGroupCode(hotel.getGroupCode());
|
||||
hb.setDataSource(SmHotelDataSource.isLocal(hotel.getSheetName()) ? "local" : "shangyou");
|
||||
hb.setDataSource(upstreamGatewayRegistry.resolveChannelKey(hotel.getSheetName()));
|
||||
hb.setUpstreamStatus(hotel.getUpstreamStatus());
|
||||
hb.setCoverImage(hotel.getCoverImage());
|
||||
hb.setAddress(hotel.getAddress());
|
||||
@@ -436,11 +447,16 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
return finishLocalOrder(order, product);
|
||||
}
|
||||
|
||||
Optional<HotelUpstreamOrderGateway> gatewayOpt = resolveUpstreamGateway(hotel, order, false);
|
||||
if (gatewayOpt.isEmpty()) {
|
||||
return R.failed(StrUtil.blankToDefault(order.getFailMsg(), "未支持的上游渠道"));
|
||||
}
|
||||
HotelUpstreamOrderGateway gateway = gatewayOpt.get();
|
||||
com.alibaba.fastjson.JSONObject upstreamBody = buildUpstreamOrderBody(order, product, body);
|
||||
|
||||
// 1) 下单前 DoubleCheck(文档 checkAvailability)
|
||||
try {
|
||||
String availMsg = doubleCheckAvailability(upstreamBody, order.getCount());
|
||||
String availMsg = doubleCheckAvailability(gateway, order, upstreamBody, order.getCount());
|
||||
if (availMsg != null) {
|
||||
order.setOrderStatus("UPSTREAM_FAIL");
|
||||
order.setFailMsg(StrUtil.sub(availMsg, 0, 500));
|
||||
@@ -454,7 +470,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
}
|
||||
|
||||
// 2) 上游下单
|
||||
ShangyouOrderCallResult call = upstreamClient.createOrderRaw(upstreamBody);
|
||||
HotelUpstreamOrderResult call = invokeCreateOrder(gateway, order, upstreamBody, "CREATE");
|
||||
applyCreateResult(order, call, upstreamBody);
|
||||
baseMapper.updateById(order);
|
||||
JSONObject out = buildOut(order);
|
||||
@@ -600,9 +616,14 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
return local;
|
||||
}
|
||||
|
||||
Optional<HotelUpstreamOrderGateway> gatewayOpt = resolveUpstreamGateway(hotel, order, false);
|
||||
if (gatewayOpt.isEmpty()) {
|
||||
return R.failed(StrUtil.blankToDefault(order.getFailMsg(), "未支持的上游渠道"));
|
||||
}
|
||||
HotelUpstreamOrderGateway gateway = gatewayOpt.get();
|
||||
com.alibaba.fastjson.JSONObject upstreamBody = buildUpstreamOrderBody(order, product, body);
|
||||
try {
|
||||
String availMsg = doubleCheckAvailability(upstreamBody, order.getCount());
|
||||
String availMsg = doubleCheckAvailability(gateway, order, upstreamBody, order.getCount());
|
||||
if (availMsg != null) {
|
||||
order.setOrderStatus("UPSTREAM_FAIL");
|
||||
order.setFailMsg(StrUtil.sub(availMsg, 0, 500));
|
||||
@@ -615,7 +636,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
order.setLastCompensateMsg("可定校验异常已忽略: " + StrUtil.sub(e.getMessage(), 0, 200));
|
||||
}
|
||||
|
||||
ShangyouOrderCallResult call = upstreamClient.createOrderRaw(upstreamBody);
|
||||
HotelUpstreamOrderResult call = invokeCreateOrder(gateway, order, upstreamBody, "CREATE");
|
||||
applyCreateResult(order, call, upstreamBody);
|
||||
baseMapper.updateById(order);
|
||||
JSONObject out = buildOut(order);
|
||||
@@ -986,7 +1007,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
}
|
||||
|
||||
private int appPayTimeoutMinutes() {
|
||||
int minutes = shangyouProperties == null ? 10 : shangyouProperties.getAppPayTimeoutMinutes();
|
||||
int minutes = channelConfigService.resolveShangyouRuntime().getAppPayTimeoutMinutes();
|
||||
return minutes > 0 ? minutes : 10;
|
||||
}
|
||||
|
||||
@@ -1046,25 +1067,36 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
return local != null && local.isOk();
|
||||
}
|
||||
|
||||
Optional<HotelUpstreamOrderGateway> gatewayOpt = resolveUpstreamGateway(hotel, order, true);
|
||||
if (gatewayOpt.isEmpty()) {
|
||||
order.setPayStatus(PAY_PAID);
|
||||
baseMapper.updateById(order);
|
||||
scheduleAutoRefundAfterUpstreamFail(order, StrUtil.blankToDefault(order.getFailMsg(), "未支持的上游渠道"));
|
||||
return true;
|
||||
}
|
||||
HotelUpstreamOrderGateway gateway = gatewayOpt.get();
|
||||
|
||||
if (product == null) {
|
||||
order.setOrderStatus("UPSTREAM_FAIL");
|
||||
order.setPayStatus(PAY_PAID);
|
||||
order.setFailMsg("支付成功但产品不存在,无法提交上游");
|
||||
baseMapper.updateById(order);
|
||||
return false;
|
||||
scheduleAutoRefundAfterUpstreamFail(order, order.getFailMsg());
|
||||
return true;
|
||||
}
|
||||
|
||||
Map<String, Object> reqBody = StrUtil.isBlank(order.getReqJson()) ? Map.of()
|
||||
: JSONUtil.parseObj(order.getReqJson());
|
||||
com.alibaba.fastjson.JSONObject upstreamBody = buildUpstreamOrderBody(order, product, reqBody);
|
||||
try {
|
||||
String availMsg = doubleCheckAvailability(upstreamBody, order.getCount());
|
||||
String availMsg = doubleCheckAvailability(gateway, order, upstreamBody, order.getCount());
|
||||
if (availMsg != null) {
|
||||
order.setOrderStatus("UPSTREAM_FAIL");
|
||||
order.setPayStatus(PAY_PAID);
|
||||
order.setFailMsg(StrUtil.sub(availMsg, 0, 500));
|
||||
baseMapper.updateById(order);
|
||||
return false;
|
||||
scheduleAutoRefundAfterUpstreamFail(order, availMsg);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
@@ -1072,15 +1104,19 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
order.setLastCompensateMsg("可定校验异常已忽略: " + StrUtil.sub(e.getMessage(), 0, 200));
|
||||
}
|
||||
|
||||
ShangyouOrderCallResult call = upstreamClient.createOrderRaw(upstreamBody);
|
||||
HotelUpstreamOrderResult call = invokeCreateOrder(gateway, order, upstreamBody, "CREATE");
|
||||
applyCreateResult(order, call, upstreamBody);
|
||||
order.setPayStatus(PAY_PAID);
|
||||
baseMapper.updateById(order);
|
||||
boolean hardFail = "UPSTREAM_FAIL".equals(order.getOrderStatus())
|
||||
&& !COMP_WAITING.equals(order.getCompensateStatus());
|
||||
log.info("|酒店支付成功|上游履约|orderId={}|status={}|hardFail={}|", orderId, order.getOrderStatus(),
|
||||
hardFail);
|
||||
// 支付已成功:即使上游暂时失败也返回 true,避免支付回调无限重试;补偿任务会继续处理
|
||||
if (hardFail) {
|
||||
scheduleAutoRefundAfterUpstreamFail(order,
|
||||
StrUtil.blankToDefault(order.getFailMsg(), "上游下单失败自动退款"));
|
||||
}
|
||||
log.info("|酒店支付成功|上游履约|orderId={}|channel={}|status={}|hardFail={}|", orderId, gateway.channel(),
|
||||
order.getOrderStatus(), hardFail);
|
||||
// 支付已成功:即使上游暂时失败也返回 true,避免支付回调无限重试;补偿/自动退款继续处理
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1361,7 +1397,8 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
/**
|
||||
* @return null=通过;非空=失败原因
|
||||
*/
|
||||
private String doubleCheckAvailability(com.alibaba.fastjson.JSONObject upstreamBody, Integer roomCount) {
|
||||
private String doubleCheckAvailability(HotelUpstreamOrderGateway gateway, SmHotelOrderEntity order,
|
||||
com.alibaba.fastjson.JSONObject upstreamBody, Integer roomCount) {
|
||||
com.alibaba.fastjson.JSONObject req = new com.alibaba.fastjson.JSONObject();
|
||||
req.put("hotelId", upstreamBody.getString("hotelId"));
|
||||
req.put("roomTypeId", upstreamBody.getString("roomTypeId"));
|
||||
@@ -1373,7 +1410,21 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
if (StrUtil.isNotBlank(upstreamBody.getString("activityId"))) {
|
||||
req.put("activityId", upstreamBody.getString("activityId"));
|
||||
}
|
||||
com.alibaba.fastjson.JSONObject content = upstreamClient.checkAvailability(req);
|
||||
long t0 = System.currentTimeMillis();
|
||||
com.alibaba.fastjson.JSONObject content;
|
||||
try {
|
||||
content = gateway.checkAvailability(req);
|
||||
}
|
||||
catch (Exception e) {
|
||||
upstreamLogService.saveSimple(order.getId(), gateway.channel(), "CHECK_AVAIL", false, "EX", e.getMessage(),
|
||||
req.toJSONString(), null, System.currentTimeMillis() - t0);
|
||||
throw e;
|
||||
}
|
||||
upstreamLogService.saveSimple(order.getId(), gateway.channel(), "CHECK_AVAIL", true, "200", "ok",
|
||||
req.toJSONString(), content == null ? null : content.toJSONString(), System.currentTimeMillis() - t0);
|
||||
if (content == null) {
|
||||
content = new com.alibaba.fastjson.JSONObject();
|
||||
}
|
||||
com.alibaba.fastjson.JSONObject roomCountObj = content.getJSONObject("roomCount");
|
||||
int need = roomCount == null ? 1 : roomCount;
|
||||
if (roomCountObj != null) {
|
||||
@@ -1407,7 +1458,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
return null;
|
||||
}
|
||||
|
||||
private void applyCreateResult(SmHotelOrderEntity order, ShangyouOrderCallResult call,
|
||||
private void applyCreateResult(SmHotelOrderEntity order, HotelUpstreamOrderResult call,
|
||||
com.alibaba.fastjson.JSONObject upstreamBody) {
|
||||
if (call.getRaw() != null) {
|
||||
order.setUpstreamRespJson(call.getRaw().toJSONString());
|
||||
@@ -1441,7 +1492,9 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
}
|
||||
|
||||
order.setPlatformNo(call.orderId());
|
||||
order.setPayStatus(call.payStatus());
|
||||
if (StrUtil.isNotBlank(call.payStatus())) {
|
||||
order.setPayStatus(call.payStatus());
|
||||
}
|
||||
String book = call.bookStatus();
|
||||
applyBookStatus(order, book, call.payStatus());
|
||||
if (ShangyouBookStatus.isProcessing(book) || StrUtil.isBlank(book)) {
|
||||
@@ -1528,11 +1581,27 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
int nextRound = round + 1;
|
||||
log.info("|酒店订单补偿|开始|orderId={}|channelNo={}|round={}|", orderId, order.getChannelNo(), nextRound);
|
||||
|
||||
ShangyouOrderCallResult query = upstreamClient.queryByRequestNoRaw(order.getChannelNo());
|
||||
SmHotelEntity hotel = order.getHotelId() == null ? null : hotelMapper.selectById(order.getHotelId());
|
||||
Optional<HotelUpstreamOrderGateway> gatewayOpt = hotel == null ? Optional.empty()
|
||||
: upstreamGatewayRegistry.find(hotel.getSheetName());
|
||||
if (gatewayOpt.isEmpty()) {
|
||||
bumpOrManual(order, nextRound, "补偿时无可用上游网关");
|
||||
return;
|
||||
}
|
||||
HotelUpstreamOrderGateway gateway = gatewayOpt.get();
|
||||
long t0 = System.currentTimeMillis();
|
||||
HotelUpstreamOrderResult query = gateway.queryByRequestNo(order.getChannelNo());
|
||||
upstreamLogService.saveCall(order.getId(), gateway.channel(), "QUERY", query, order.getChannelNo(),
|
||||
System.currentTimeMillis() - t0);
|
||||
|
||||
if (query.isUncertain() || (!query.isBizOk() && StrUtil.isBlank(query.orderId()))) {
|
||||
// 查不到确定结果:文档允许用原 requestNo 重试 order/add
|
||||
if (nextRound <= 2) {
|
||||
ShangyouOrderCallResult retryAdd = retryOrderAdd(order);
|
||||
HotelUpstreamOrderResult retryAdd = retryOrderAdd(gateway, order);
|
||||
if (retryAdd != null && !retryAdd.isUncertain() && !retryAdd.isBizOk() && !retryAdd.isPayFail3028()) {
|
||||
applyHardUpstreamFailFromCompensate(order, retryAdd);
|
||||
return;
|
||||
}
|
||||
if (retryAdd != null && (retryAdd.isBizOk() || StrUtil.isNotBlank(retryAdd.orderId()))) {
|
||||
applyCompensateSuccess(order, retryAdd, nextRound, "重试 order/add");
|
||||
return;
|
||||
@@ -1544,7 +1613,11 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
|
||||
if (StrUtil.isBlank(query.orderId()) && StrUtil.isBlank(query.bookStatus())) {
|
||||
// 尚无订单:尝试重试下单
|
||||
ShangyouOrderCallResult retryAdd = retryOrderAdd(order);
|
||||
HotelUpstreamOrderResult retryAdd = retryOrderAdd(gateway, order);
|
||||
if (retryAdd != null && !retryAdd.isUncertain() && !retryAdd.isBizOk() && !retryAdd.isPayFail3028()) {
|
||||
applyHardUpstreamFailFromCompensate(order, retryAdd);
|
||||
return;
|
||||
}
|
||||
if (retryAdd != null && (retryAdd.isBizOk() || StrUtil.isNotBlank(retryAdd.orderId()))) {
|
||||
applyCompensateSuccess(order, retryAdd, nextRound, "查询无单后重试 order/add");
|
||||
return;
|
||||
@@ -1556,7 +1629,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
applyCompensateSuccess(order, query, nextRound, "queryByRequestNo");
|
||||
}
|
||||
|
||||
private ShangyouOrderCallResult retryOrderAdd(SmHotelOrderEntity order) {
|
||||
private HotelUpstreamOrderResult retryOrderAdd(HotelUpstreamOrderGateway gateway, SmHotelOrderEntity order) {
|
||||
try {
|
||||
Map<String, Object> body = StrUtil.isBlank(order.getReqJson()) ? Map.of()
|
||||
: JSONUtil.parseObj(order.getReqJson());
|
||||
@@ -1568,15 +1641,30 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
.last("limit 1"));
|
||||
}
|
||||
com.alibaba.fastjson.JSONObject upstreamBody = buildUpstreamOrderBody(order, product, body);
|
||||
return upstreamClient.createOrderRaw(upstreamBody);
|
||||
return invokeCreateOrder(gateway, order, upstreamBody, "RETRY_ADD");
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|酒店订单补偿|重试下单异常|channelNo={}|{}", order.getChannelNo(), e.getMessage());
|
||||
return ShangyouOrderCallResult.uncertain(e.getMessage(), null);
|
||||
return HotelUpstreamOrderResult.uncertain(e.getMessage(), null);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyCompensateSuccess(SmHotelOrderEntity order, ShangyouOrderCallResult call, int round,
|
||||
/** 补偿中明确业务失败:已支付则自动退款;NEED_MANUAL(不明)不走此路径 */
|
||||
private void applyHardUpstreamFailFromCompensate(SmHotelOrderEntity order, HotelUpstreamOrderResult call) {
|
||||
order.setOrderStatus("UPSTREAM_FAIL");
|
||||
order.setFailMsg(StrUtil.sub("补偿确认失败 code=" + call.getCode() + " " + call.getMessage(), 0, 500));
|
||||
order.setCompensateStatus(COMP_NONE);
|
||||
order.setNextCompensateTime(null);
|
||||
if (call.getRaw() != null) {
|
||||
order.setUpstreamRespJson(call.getRaw().toJSONString());
|
||||
}
|
||||
baseMapper.updateById(order);
|
||||
if (PAY_PAID.equals(order.getPayStatus())) {
|
||||
scheduleAutoRefundAfterUpstreamFail(order, order.getFailMsg());
|
||||
}
|
||||
}
|
||||
|
||||
private void applyCompensateSuccess(SmHotelOrderEntity order, HotelUpstreamOrderResult call, int round,
|
||||
String source) {
|
||||
if (call.getRaw() != null) {
|
||||
order.setUpstreamRespJson(call.getRaw().toJSONString());
|
||||
@@ -1706,21 +1794,11 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
if (StrUtil.isBlank(order.getPlatformNo())) {
|
||||
return R.failed("尚无上游订单号,无法取消;若在补偿中请等待或人工确认");
|
||||
}
|
||||
try {
|
||||
boolean ok = upstreamClient.cancelOrder(order.getPlatformNo(), order.getHotelCode());
|
||||
if (!ok) {
|
||||
return R.failed("上游取消返回失败");
|
||||
}
|
||||
applyBookStatus(order, ShangyouBookStatus.X, order.getPayStatus());
|
||||
order.setOrderStatus("CANCELLED");
|
||||
order.setCompensateStatus(COMP_DONE);
|
||||
order.setNextCompensateTime(null);
|
||||
baseMapper.updateById(order);
|
||||
return R.ok(buildOut(order));
|
||||
}
|
||||
catch (Exception e) {
|
||||
return R.failed("上游取消失败: " + e.getMessage());
|
||||
R<Boolean> cancelR = cancelUpstreamOrder(order, "下游取消");
|
||||
if (cancelR == null || !cancelR.isOk()) {
|
||||
return R.failed(cancelR == null ? "上游取消失败" : cancelR.getMsg());
|
||||
}
|
||||
return R.ok(buildOut(baseMapper.selectById(order.getId())));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1736,11 +1814,22 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
// 有平台号则刷新一次上游状态
|
||||
try {
|
||||
if (StrUtil.isNotBlank(order.getPlatformNo())) {
|
||||
com.alibaba.fastjson.JSONObject content = upstreamClient.queryOrder(order.getPlatformNo(),
|
||||
order.getHotelCode());
|
||||
applyBookStatus(order, content.getString("bookStatus"), content.getString("payStatus"));
|
||||
order.setUpstreamRespJson(content.toJSONString());
|
||||
baseMapper.updateById(order);
|
||||
SmHotelEntity hotel = order.getHotelId() == null ? null : hotelMapper.selectById(order.getHotelId());
|
||||
Optional<HotelUpstreamOrderGateway> gatewayOpt = hotel == null ? Optional.empty()
|
||||
: upstreamGatewayRegistry.find(hotel.getSheetName());
|
||||
if (gatewayOpt.isPresent()) {
|
||||
HotelUpstreamOrderGateway gateway = gatewayOpt.get();
|
||||
long t0 = System.currentTimeMillis();
|
||||
HotelUpstreamOrderResult q = gateway.queryOrder(order.getPlatformNo(), order.getHotelCode());
|
||||
upstreamLogService.saveCall(order.getId(), gateway.channel(), "QUERY", q, order.getPlatformNo(),
|
||||
System.currentTimeMillis() - t0);
|
||||
if (q.getContent() != null) {
|
||||
applyBookStatus(order, q.getContent().getString("bookStatus"),
|
||||
q.getContent().getString("payStatus"));
|
||||
order.setUpstreamRespJson(q.getContent().toJSONString());
|
||||
baseMapper.updateById(order);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (COMP_WAITING.equals(order.getCompensateStatus())
|
||||
|| "PENDING_CONFIRM".equals(order.getOrderStatus())) {
|
||||
@@ -1803,24 +1892,11 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
baseMapper.updateById(order);
|
||||
return R.ok(true, PAY_UNPAID.equals(order.getPayStatus()) ? "未支付订单已取消" : "本地订单已关闭");
|
||||
}
|
||||
try {
|
||||
boolean ok = upstreamClient.cancelOrder(order.getPlatformNo(), order.getHotelCode());
|
||||
if (!ok) {
|
||||
return R.failed("上游取消返回失败");
|
||||
}
|
||||
applyBookStatus(order, ShangyouBookStatus.X, order.getPayStatus());
|
||||
order.setOrderStatus("CANCELLED");
|
||||
order.setCompensateStatus(COMP_DONE);
|
||||
order.setNextCompensateTime(null);
|
||||
if (StrUtil.isNotBlank(reason)) {
|
||||
order.setFailMsg(StrUtil.sub(reason, 0, 200));
|
||||
}
|
||||
baseMapper.updateById(order);
|
||||
return R.ok(true);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return R.failed("取消失败: " + e.getMessage());
|
||||
R<Boolean> cancelR = cancelUpstreamOrder(order, reason);
|
||||
if (cancelR == null || !cancelR.isOk()) {
|
||||
return R.failed(cancelR == null ? "取消失败" : cancelR.getMsg());
|
||||
}
|
||||
return R.ok(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1952,7 +2028,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
|
||||
boolean isGroup = resolveOrderIsGroup(downstreamBody);
|
||||
String guarantee = StrUtil.blankToDefault(str(downstreamBody.get("guaranteeMode")),
|
||||
StrUtil.blankToDefault(shangyouProperties.getOrderGuaranteeMode(), "NONE"));
|
||||
StrUtil.blankToDefault(channelConfigService.resolveShangyouRuntime().getOrderGuaranteeMode(), "NONE"));
|
||||
req.put("guaranteeMode", guarantee);
|
||||
req.put("isGroup", isGroup);
|
||||
req.put("apiType", "INTERFACE");
|
||||
@@ -2027,7 +2103,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
if (v != null && StrUtil.isNotBlank(String.valueOf(v))) {
|
||||
return Boolean.parseBoolean(String.valueOf(v));
|
||||
}
|
||||
return shangyouProperties.isOrderIsGroup();
|
||||
return channelConfigService.resolveShangyouRuntime().isOrderIsGroup();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2126,4 +2202,108 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
}
|
||||
return new BigDecimal(String.valueOf(o));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析上游网关;未注册渠道时落 UPSTREAM_FAIL。
|
||||
*
|
||||
* @param scheduleRefundOnUnsupported App 已付场景由调用方在 empty 后自行退款;此处仅落失败状态
|
||||
*/
|
||||
private Optional<HotelUpstreamOrderGateway> resolveUpstreamGateway(SmHotelEntity hotel, SmHotelOrderEntity order,
|
||||
boolean scheduleRefundOnUnsupported) {
|
||||
if (hotel == null) {
|
||||
order.setOrderStatus("UPSTREAM_FAIL");
|
||||
order.setFailMsg("酒店不存在,无法提交上游");
|
||||
order.setCompensateStatus(COMP_NONE);
|
||||
baseMapper.updateById(order);
|
||||
return Optional.empty();
|
||||
}
|
||||
if (upstreamGatewayRegistry.isUnsupportedChannel(hotel.getSheetName())) {
|
||||
String ch = upstreamGatewayRegistry.resolveChannelKey(hotel.getSheetName());
|
||||
order.setOrderStatus("UPSTREAM_FAIL");
|
||||
order.setFailMsg("未接入的上游渠道: " + ch);
|
||||
order.setCompensateStatus(COMP_NONE);
|
||||
baseMapper.updateById(order);
|
||||
upstreamLogService.saveSimple(order.getId(), ch, "CREATE", false, "UNSUPPORTED",
|
||||
order.getFailMsg(), null, null, 0L);
|
||||
if (scheduleRefundOnUnsupported) {
|
||||
// 退款由调用方在置 PAID 后触发
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
Optional<HotelUpstreamOrderGateway> gateway = upstreamGatewayRegistry.find(hotel.getSheetName());
|
||||
if (gateway.isEmpty()) {
|
||||
order.setOrderStatus("UPSTREAM_FAIL");
|
||||
order.setFailMsg("本地酒店不应走上游履约");
|
||||
order.setCompensateStatus(COMP_NONE);
|
||||
baseMapper.updateById(order);
|
||||
}
|
||||
return gateway;
|
||||
}
|
||||
|
||||
private HotelUpstreamOrderResult invokeCreateOrder(HotelUpstreamOrderGateway gateway, SmHotelOrderEntity order,
|
||||
com.alibaba.fastjson.JSONObject upstreamBody, String action) {
|
||||
long t0 = System.currentTimeMillis();
|
||||
HotelUpstreamOrderResult call;
|
||||
try {
|
||||
call = gateway.createOrder(upstreamBody);
|
||||
}
|
||||
catch (Exception e) {
|
||||
call = HotelUpstreamOrderResult.uncertain(e.getMessage(), null);
|
||||
}
|
||||
upstreamLogService.saveCall(order.getId(), gateway.channel(), action, call,
|
||||
upstreamBody == null ? null : upstreamBody.toJSONString(), System.currentTimeMillis() - t0);
|
||||
return call;
|
||||
}
|
||||
|
||||
/** 事务提交后触发自动退款(仅已支付订单) */
|
||||
private void scheduleAutoRefundAfterUpstreamFail(SmHotelOrderEntity order, String reason) {
|
||||
if (order == null || order.getId() == null) {
|
||||
return;
|
||||
}
|
||||
SmHotelOrderEntity latest = baseMapper.selectById(order.getId());
|
||||
if (latest == null) {
|
||||
return;
|
||||
}
|
||||
if (!PAY_PAID.equals(latest.getPayStatus())) {
|
||||
log.info("|酒店自动退款|跳过非已支付|orderId={}|payStatus={}|", latest.getId(), latest.getPayStatus());
|
||||
return;
|
||||
}
|
||||
Long tenantId = latest.getTenantId() != null ? latest.getTenantId() : TenantContextHolder.getTenantId();
|
||||
eventPublisher.publishEvent(new HotelUpstreamFailRefundEvent(latest.getId(), tenantId,
|
||||
StrUtil.blankToDefault(reason, "上游下单失败自动退款")));
|
||||
}
|
||||
|
||||
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()
|
||||
: upstreamGatewayRegistry.find(hotel.getSheetName());
|
||||
if (gatewayOpt.isEmpty()) {
|
||||
return R.failed("无可用上游网关,无法取消");
|
||||
}
|
||||
HotelUpstreamOrderGateway gateway = gatewayOpt.get();
|
||||
long t0 = System.currentTimeMillis();
|
||||
HotelUpstreamOrderResult call = gateway.cancelOrder(order.getPlatformNo(), order.getHotelCode());
|
||||
upstreamLogService.saveCall(order.getId(), gateway.channel(), "CANCEL", call,
|
||||
order.getPlatformNo(), System.currentTimeMillis() - t0);
|
||||
if (call.isUncertain()) {
|
||||
return R.failed("上游取消不确定: " + call.getMessage());
|
||||
}
|
||||
if (!call.isBizOk()) {
|
||||
return R.failed(StrUtil.blankToDefault(call.getMessage(), "上游取消返回失败"));
|
||||
}
|
||||
applyBookStatus(order, ShangyouBookStatus.X, order.getPayStatus());
|
||||
order.setOrderStatus("CANCELLED");
|
||||
order.setCompensateStatus(COMP_DONE);
|
||||
order.setNextCompensateTime(null);
|
||||
if (StrUtil.isNotBlank(reason)) {
|
||||
order.setFailMsg(StrUtil.sub(reason, 0, 200));
|
||||
}
|
||||
baseMapper.updateById(order);
|
||||
return R.ok(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SmHotelOrderUpstreamLogEntity> listUpstreamLogs(Long orderId) {
|
||||
return upstreamLogService.listByOrderId(orderId);
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package com.bsd.pjcloud.supermarket.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelOrderUpstreamLogEntity;
|
||||
import com.bsd.pjcloud.supermarket.integration.hotel.upstream.HotelUpstreamOrderResult;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelOrderUpstreamLogMapper;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelOrderUpstreamLogService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SmHotelOrderUpstreamLogServiceImpl
|
||||
extends ServiceImpl<SmHotelOrderUpstreamLogMapper, SmHotelOrderUpstreamLogEntity>
|
||||
implements SmHotelOrderUpstreamLogService {
|
||||
|
||||
private static final int JSON_MAX = 8000;
|
||||
|
||||
@Override
|
||||
public void saveCall(Long orderId, String channel, String action, HotelUpstreamOrderResult result, String reqJson,
|
||||
long costMs) {
|
||||
if (orderId == null || result == null) {
|
||||
return;
|
||||
}
|
||||
boolean success = result.isBizOk() && !result.isUncertain();
|
||||
String resp = null;
|
||||
if (result.getRaw() != null) {
|
||||
resp = result.getRaw().toJSONString();
|
||||
}
|
||||
else if (result.getContent() != null) {
|
||||
resp = result.getContent().toJSONString();
|
||||
}
|
||||
saveSimple(orderId, channel, action, success, result.getCode(),
|
||||
StrUtil.blankToDefault(result.getMessage(), result.getExceptionMsg()), reqJson, resp, costMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveSimple(Long orderId, String channel, String action, boolean success, String bizCode, String message,
|
||||
String reqJson, String respJson, Long costMs) {
|
||||
if (orderId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
SmHotelOrderUpstreamLogEntity row = new SmHotelOrderUpstreamLogEntity();
|
||||
row.setOrderId(orderId);
|
||||
row.setChannel(StrUtil.blankToDefault(channel, "-"));
|
||||
row.setAction(StrUtil.blankToDefault(action, "-"));
|
||||
row.setSuccess(success ? "1" : "0");
|
||||
row.setBizCode(StrUtil.sub(bizCode, 0, 64));
|
||||
row.setMessage(StrUtil.sub(message, 0, 500));
|
||||
row.setReqJson(clip(reqJson));
|
||||
row.setRespJson(clip(respJson));
|
||||
row.setCostMs(costMs);
|
||||
baseMapper.insert(row);
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|酒店上游流水|写入失败|orderId={}|action={}|{}", orderId, action, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SmHotelOrderUpstreamLogEntity> listByOrderId(Long orderId) {
|
||||
if (orderId == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return baseMapper.selectList(Wrappers.<SmHotelOrderUpstreamLogEntity>lambdaQuery()
|
||||
.eq(SmHotelOrderUpstreamLogEntity::getOrderId, orderId)
|
||||
.orderByAsc(SmHotelOrderUpstreamLogEntity::getCreateTime));
|
||||
}
|
||||
|
||||
private static String clip(String s) {
|
||||
if (s == null) {
|
||||
return null;
|
||||
}
|
||||
return s.length() <= JSON_MAX ? s : s.substring(0, JSON_MAX) + "...(truncated)";
|
||||
}
|
||||
}
|
||||
+111
-61
@@ -13,6 +13,8 @@ import com.bsd.pjcloud.supermarket.integration.shangyou.ShangyouHotelApi;
|
||||
import com.bsd.pjcloud.supermarket.integration.shangyou.ShangyouHotelClient;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelMapper;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelSyncLogMapper;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelConfigService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelDeptService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelMidService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelSyncService;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
@@ -68,12 +70,19 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
return t;
|
||||
});
|
||||
|
||||
private final ShangyouHotelProperties properties;
|
||||
private final SmHotelChannelConfigService channelConfigService;
|
||||
|
||||
private final ShangyouHotelClient upstreamClient;
|
||||
|
||||
/** 库表覆盖 YAML 后的生效配置(日更开关等热更新) */
|
||||
private ShangyouHotelProperties props() {
|
||||
return channelConfigService.resolveShangyouRuntime();
|
||||
}
|
||||
|
||||
private final SmHotelMidService midService;
|
||||
|
||||
private final SmHotelChannelDeptService channelDeptService;
|
||||
|
||||
private final SmHotelMapper hotelMapper;
|
||||
|
||||
private final SmHotelSyncLogMapper syncLogMapper;
|
||||
@@ -90,12 +99,30 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
return R.failed("已有列表同步任务进行中,请稍候查看进度(若长时间卡住请在同步日志点「终止」)");
|
||||
}
|
||||
String source = resolveImportDataSource(dataSource, triggerType);
|
||||
if ("MANUAL".equalsIgnoreCase(StrUtil.trim(triggerType))) {
|
||||
R<Long> deptR = channelDeptService.requireEnabledDeptId(source);
|
||||
if (deptR == null || !deptR.isOk()) {
|
||||
return R.failed(deptR == null ? "渠道部门未配置" : deptR.getMsg());
|
||||
}
|
||||
}
|
||||
else if (StrUtil.isNotBlank(source) && channelDeptService.resolveDeptId(source) == null) {
|
||||
log.warn("|酒店列表同步|定时任务渠道未配置部门|channel={}|将导入但 deptId 为空|", source);
|
||||
}
|
||||
if (StrUtil.isBlank(source)) {
|
||||
return R.failed("请选择数据来源(字典 sm_hotel_data_source)");
|
||||
}
|
||||
if (SmHotelDataSource.isLocal(source)) {
|
||||
return R.failed("上游同步不能使用「本地」数据来源");
|
||||
}
|
||||
if ("MANUAL".equalsIgnoreCase(StrUtil.trim(triggerType))) {
|
||||
R<Long> deptR = channelDeptService.requireEnabledDeptId(source);
|
||||
if (deptR == null || !deptR.isOk()) {
|
||||
return R.failed(deptR == null ? "渠道部门未配置" : deptR.getMsg());
|
||||
}
|
||||
}
|
||||
else if (channelDeptService.resolveDeptId(source) == null) {
|
||||
log.warn("|酒店列表同步|定时任务渠道未配置部门|channel={}|导入时可能无 deptId|", source);
|
||||
}
|
||||
// 供差集「补导剩余新增」复用本次选择的来源
|
||||
redisTemplate.opsForValue().set(ShangyouHotelApi.REDIS_IMPORT_DATA_SOURCE, source, 2, TimeUnit.DAYS);
|
||||
|
||||
@@ -107,7 +134,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
if (tenantId == null) {
|
||||
tenantId = properties.getCallbackTenantId() == null ? 1L : properties.getCallbackTenantId();
|
||||
tenantId = props().getCallbackTenantId() == null ? 1L : props().getCallbackTenantId();
|
||||
}
|
||||
final Long tid = tenantId;
|
||||
final Long logId = logEntity.getId();
|
||||
@@ -119,7 +146,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
|
||||
@Override
|
||||
public R<SmHotelSyncLogEntity> startPriceRollAsync(String triggerType) {
|
||||
if (!properties.isPriceRollDailyEnabled() && !"MANUAL".equalsIgnoreCase(triggerType)) {
|
||||
if (!props().isPriceRollDailyEnabled() && !"MANUAL".equalsIgnoreCase(triggerType)) {
|
||||
return R.failed("滚价未启用");
|
||||
}
|
||||
abortStaleRunningSyncLogs();
|
||||
@@ -136,7 +163,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
syncLogMapper.insert(logEntity);
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
if (tenantId == null) {
|
||||
tenantId = properties.getCallbackTenantId() == null ? 1L : properties.getCallbackTenantId();
|
||||
tenantId = props().getCallbackTenantId() == null ? 1L : props().getCallbackTenantId();
|
||||
}
|
||||
final Long tid = tenantId;
|
||||
final Long logId = logEntity.getId();
|
||||
@@ -146,7 +173,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
|
||||
@Override
|
||||
public R<SmHotelSyncLogEntity> startLocalDailyRollAsync(String triggerType) {
|
||||
if (!properties.isLocalDailyRollEnabled() && !"MANUAL".equalsIgnoreCase(triggerType)) {
|
||||
if (!props().isLocalDailyRollEnabled() && !"MANUAL".equalsIgnoreCase(triggerType)) {
|
||||
return R.failed("本地自动滚加未启用");
|
||||
}
|
||||
abortStaleRunningSyncLogs();
|
||||
@@ -163,7 +190,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
syncLogMapper.insert(logEntity);
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
if (tenantId == null) {
|
||||
tenantId = properties.getCallbackTenantId() == null ? 1L : properties.getCallbackTenantId();
|
||||
tenantId = props().getCallbackTenantId() == null ? 1L : props().getCallbackTenantId();
|
||||
}
|
||||
final Long tid = tenantId;
|
||||
final Long logId = logEntity.getId();
|
||||
@@ -172,7 +199,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<SmHotelSyncLogEntity> startDetailPullAsync(Integer limit, String triggerType) {
|
||||
public R<SmHotelSyncLogEntity> startDetailPullAsync(Integer limit, String triggerType, String dataSource) {
|
||||
abortStaleRunningSyncLogs();
|
||||
Long running = syncLogMapper.selectCount(Wrappers.<SmHotelSyncLogEntity>lambdaQuery()
|
||||
.eq(SmHotelSyncLogEntity::getSyncType, TYPE_DETAIL)
|
||||
@@ -180,28 +207,36 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
if (running != null && running > 0) {
|
||||
return R.failed("已有全量详情拉取任务进行中,请打开「同步日志」查看进度(若长时间卡住请点「终止」)");
|
||||
}
|
||||
R<Long> deptR = channelDeptService.requireEnabledDeptId(dataSource);
|
||||
if (deptR == null || !deptR.isOk() || deptR.getData() == null) {
|
||||
return R.failed(deptR == null ? "渠道部门未配置" : deptR.getMsg());
|
||||
}
|
||||
String channel = SmHotelDataSource.normalize(dataSource).toLowerCase();
|
||||
Long deptId = deptR.getData();
|
||||
String quotaErr = checkFullPriceSyncQuota();
|
||||
if (quotaErr != null) {
|
||||
return R.failed(quotaErr);
|
||||
}
|
||||
|
||||
int maxHotels = limit != null && limit > 0 ? limit : properties.getDetailPullMaxHotels();
|
||||
int maxHotels = limit != null && limit > 0 ? limit : props().getDetailPullMaxHotels();
|
||||
SmHotelSyncLogEntity logEntity = newLog(TYPE_DETAIL, triggerType);
|
||||
logEntity.setStage("QUEUED");
|
||||
logEntity.setStageMsg(maxHotels > 0 ? "已提交,最多处理 " + maxHotels + " 家启用酒店…"
|
||||
: "已提交,将拉取全部启用酒店详情(耗时长,请看进度)…");
|
||||
logEntity.setStageMsg((maxHotels > 0 ? "已提交,最多处理 " + maxHotels + " 家" : "已提交,将拉取")
|
||||
+ "「" + channel + "」启用酒店详情,归属部门=" + deptId + "…");
|
||||
logEntity.setProgressPct(1);
|
||||
syncLogMapper.insert(logEntity);
|
||||
markFullPriceSyncStarted();
|
||||
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
if (tenantId == null) {
|
||||
tenantId = properties.getCallbackTenantId() == null ? 1L : properties.getCallbackTenantId();
|
||||
tenantId = props().getCallbackTenantId() == null ? 1L : props().getCallbackTenantId();
|
||||
}
|
||||
final Long tid = tenantId;
|
||||
final Long logId = logEntity.getId();
|
||||
final int cap = maxHotels;
|
||||
EXECUTOR.execute(() -> TenantBroker.runAs(tid, id -> runDetailPull(logId, cap)));
|
||||
final String ch = channel;
|
||||
final Long dept = deptId;
|
||||
EXECUTOR.execute(() -> TenantBroker.runAs(tid, id -> runDetailPull(logId, cap, ch, dept)));
|
||||
return R.ok(logEntity, "已提交全量详情拉取");
|
||||
}
|
||||
|
||||
@@ -243,7 +278,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
if (tenantId == null) {
|
||||
tenantId = properties.getCallbackTenantId() == null ? 1L : properties.getCallbackTenantId();
|
||||
tenantId = props().getCallbackTenantId() == null ? 1L : props().getCallbackTenantId();
|
||||
}
|
||||
final Long tid = tenantId;
|
||||
final Long logId = logEntity.getId();
|
||||
@@ -351,8 +386,8 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
redisTemplate.expire(ShangyouHotelApi.REDIS_DIFF_OFFLINE_SET, 2, TimeUnit.DAYS);
|
||||
|
||||
int imported = 0;
|
||||
if (autoImport && properties.isAutoImportNewHotels()) {
|
||||
int batchSize = Math.max(1, properties.getAutoImportBatchSize());
|
||||
if (autoImport && props().isAutoImportNewHotels()) {
|
||||
int batchSize = Math.max(1, props().getAutoImportBatchSize());
|
||||
int round = 0;
|
||||
// 按批循环直到差集清空;batchSize 只控内存,不再当「只导一批」上限
|
||||
while (round < 10000) {
|
||||
@@ -381,12 +416,12 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
}
|
||||
|
||||
int offlineMarked = 0;
|
||||
if (properties.isAutoMarkOfflineHotels()) {
|
||||
if (props().isAutoMarkOfflineHotels()) {
|
||||
progress(logId, 80, "MARK_OFFLINE", "正在将上游下线酒店标为停用…");
|
||||
offlineMarked = markOfflineFromDiff();
|
||||
}
|
||||
int reactivated = 0;
|
||||
if (properties.isAutoReactivateOnlineHotels()) {
|
||||
if (props().isAutoReactivateOnlineHotels()) {
|
||||
progress(logId, 90, "REACTIVATE", "正在恢复重新授权的酒店…");
|
||||
reactivated = reactivateBackOnlineHotels();
|
||||
}
|
||||
@@ -423,10 +458,10 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
long t0 = System.currentTimeMillis();
|
||||
try {
|
||||
progress(logId, 5, "ROLL", "开始滚动拉取第 N 天房价…");
|
||||
int offset = Math.max(1, properties.getPriceRollDayOffset());
|
||||
int offset = Math.max(1, props().getPriceRollDayOffset());
|
||||
LocalDate day = LocalDate.now().plusDays(offset);
|
||||
LocalDate end = day.plusDays(1);
|
||||
int batchSize = Math.max(20, properties.getPriceRollBatchSize());
|
||||
int batchSize = Math.max(20, props().getPriceRollBatchSize());
|
||||
long lastId = 0L;
|
||||
int ok = 0;
|
||||
int fail = 0;
|
||||
@@ -480,7 +515,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
fail++;
|
||||
appendFail(failLines, h, e.getMessage());
|
||||
}
|
||||
int intervalMs = Math.max(0, properties.getPriceRollIntervalMs());
|
||||
int intervalMs = Math.max(0, props().getPriceRollIntervalMs());
|
||||
if (intervalMs > 0) {
|
||||
try {
|
||||
Thread.sleep(intervalMs);
|
||||
@@ -530,7 +565,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
long t0 = System.currentTimeMillis();
|
||||
try {
|
||||
progress(logId, 5, "ROLL", "开始本地滚加(补齐缺失日历)…");
|
||||
int batchSize = Math.max(20, properties.getPriceRollBatchSize());
|
||||
int batchSize = Math.max(20, props().getPriceRollBatchSize());
|
||||
long lastId = 0L;
|
||||
int ok = 0;
|
||||
int fail = 0;
|
||||
@@ -635,21 +670,27 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
* 对商游来源启用酒店逐家 pullFromUpstream(综合信息 + 未来 price-days 房价)。
|
||||
* 本地及其它渠道酒店一律不进此任务。
|
||||
*/
|
||||
private void runDetailPull(Long logId, int maxHotels) {
|
||||
private void runDetailPull(Long logId, int maxHotels, String channel, Long deptId) {
|
||||
long t0 = System.currentTimeMillis();
|
||||
String ch = SmHotelDataSource.normalize(channel).toLowerCase();
|
||||
try {
|
||||
progress(logId, 3, "SCAN", "正在统计商游启用酒店…");
|
||||
// 将该渠道酒店统一归属到维护的部门(便于支付商户/数据权限)
|
||||
if (deptId != null && StrUtil.isNotBlank(ch)) {
|
||||
hotelMapper.update(null, Wrappers.<SmHotelEntity>lambdaUpdate()
|
||||
.set(SmHotelEntity::getDeptId, deptId)
|
||||
.apply(channelSheetSql(ch), ch)
|
||||
.and(w -> w.isNull(SmHotelEntity::getDeptId).or().ne(SmHotelEntity::getDeptId, deptId)));
|
||||
}
|
||||
progress(logId, 3, "SCAN", "正在统计「" + ch + "」启用酒店…");
|
||||
Long totalHint = hotelMapper.selectCount(Wrappers.<SmHotelEntity>lambdaQuery()
|
||||
.and(w -> w.isNull(SmHotelEntity::getUpstreamStatus)
|
||||
.or()
|
||||
.ne(SmHotelEntity::getUpstreamStatus, "OFFLINE"))
|
||||
.and(w -> w.isNull(SmHotelEntity::getSheetName)
|
||||
.or()
|
||||
.eq(SmHotelEntity::getSheetName, SmHotelDataSource.SHANGYOU)));
|
||||
.apply(channelSheetSql(ch), ch));
|
||||
long totalAll = totalHint == null ? 0L : totalHint;
|
||||
long target = maxHotels > 0 ? Math.min(totalAll, maxHotels) : totalAll;
|
||||
int batchSize = Math.max(20, properties.getPriceRollBatchSize());
|
||||
int intervalMs = Math.max(0, properties.getDetailPullIntervalMs());
|
||||
int batchSize = Math.max(20, props().getPriceRollBatchSize());
|
||||
int intervalMs = Math.max(0, props().getDetailPullIntervalMs());
|
||||
long lastId = 0L;
|
||||
int ok = 0;
|
||||
int fail = 0;
|
||||
@@ -657,8 +698,8 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
int processed = 0;
|
||||
List<String> failLines = new ArrayList<>();
|
||||
|
||||
progress(logId, 5, "PULL", "开始拉取详情:目标约 " + target + " 家商游酒店(price-days="
|
||||
+ properties.getPriceDays() + ")…");
|
||||
progress(logId, 5, "PULL", "开始拉取详情:目标约 " + target + " 家「" + ch + "」酒店(price-days="
|
||||
+ props().getPriceDays() + ")…");
|
||||
|
||||
outer:
|
||||
while (true) {
|
||||
@@ -669,9 +710,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
.and(w -> w.isNull(SmHotelEntity::getUpstreamStatus)
|
||||
.or()
|
||||
.ne(SmHotelEntity::getUpstreamStatus, "OFFLINE"))
|
||||
.and(w -> w.isNull(SmHotelEntity::getSheetName)
|
||||
.or()
|
||||
.eq(SmHotelEntity::getSheetName, SmHotelDataSource.SHANGYOU))
|
||||
.apply(channelSheetSql(ch), ch)
|
||||
.orderByAsc(SmHotelEntity::getId)
|
||||
.last("limit " + batchSize));
|
||||
if (CollUtil.isEmpty(rows)) {
|
||||
@@ -773,9 +812,9 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
int processed = 0;
|
||||
List<String> failLines = new ArrayList<>();
|
||||
int intervalMs = TYPE_ROLL.equals(syncType)
|
||||
? Math.max(0, properties.getPriceRollIntervalMs())
|
||||
: Math.max(0, properties.getDetailPullIntervalMs());
|
||||
LocalDate day = rollDay != null ? rollDay : LocalDate.now().plusDays(Math.max(1, properties.getPriceRollDayOffset()));
|
||||
? Math.max(0, props().getPriceRollIntervalMs())
|
||||
: Math.max(0, props().getDetailPullIntervalMs());
|
||||
LocalDate day = rollDay != null ? rollDay : LocalDate.now().plusDays(Math.max(1, props().getPriceRollDayOffset()));
|
||||
LocalDate end = day.plusDays(1);
|
||||
|
||||
progress(logId, 8, "RETRY", "开始重试 " + target + " 家"
|
||||
@@ -885,7 +924,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
return LocalDate.now().plusDays(Math.max(1, properties.getPriceRollDayOffset()));
|
||||
return LocalDate.now().plusDays(Math.max(1, props().getPriceRollDayOffset()));
|
||||
}
|
||||
|
||||
private static void appendFail(List<String> failLines, SmHotelEntity h, String reason) {
|
||||
@@ -916,8 +955,8 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
|
||||
/** 文档限制:全量房价每天次数 + 最小间隔 */
|
||||
private String checkFullPriceSyncQuota() {
|
||||
int maxPerDay = Math.max(1, properties.getFullPriceSyncMaxPerDay());
|
||||
int minHours = Math.max(0, properties.getFullPriceSyncMinIntervalHours());
|
||||
int maxPerDay = Math.max(1, props().getFullPriceSyncMaxPerDay());
|
||||
int minHours = Math.max(0, props().getFullPriceSyncMinIntervalHours());
|
||||
String last = redisTemplate.opsForValue().get(ShangyouHotelApi.REDIS_FULL_PRICE_SYNC_LAST);
|
||||
if (StrUtil.isNotBlank(last) && minHours > 0) {
|
||||
try {
|
||||
@@ -1009,7 +1048,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
|
||||
private int markOfflineFromDiff() {
|
||||
int updated = 0;
|
||||
int batch = Math.max(50, properties.getDiffScanBatchSize());
|
||||
int batch = Math.max(50, props().getDiffScanBatchSize());
|
||||
// 不用 SET.scan:Redisson 与 Spring Data Redis 3.3 的 Cursor 不兼容会抛 AbstractMethodError
|
||||
Set<String> all = redisTemplate.opsForSet().members(ShangyouHotelApi.REDIS_DIFF_OFFLINE_SET);
|
||||
if (all == null || all.isEmpty()) {
|
||||
@@ -1035,7 +1074,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
private int reactivateBackOnlineHotels() {
|
||||
int updated = 0;
|
||||
long lastId = 0L;
|
||||
int batch = Math.max(50, properties.getDiffScanBatchSize());
|
||||
int batch = Math.max(50, props().getDiffScanBatchSize());
|
||||
while (true) {
|
||||
List<SmHotelEntity> rows = hotelMapper.selectList(Wrappers.<SmHotelEntity>lambdaQuery()
|
||||
.select(SmHotelEntity::getId, SmHotelEntity::getHotelCode)
|
||||
@@ -1131,12 +1170,12 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
source = redisTemplate.opsForValue().get(ShangyouHotelApi.REDIS_IMPORT_DATA_SOURCE);
|
||||
}
|
||||
if (StrUtil.isBlank(source)) {
|
||||
source = StrUtil.trim(properties.getDefaultImportDataSource());
|
||||
source = StrUtil.trim(props().getDefaultImportDataSource());
|
||||
}
|
||||
if (StrUtil.isBlank(source)) {
|
||||
return R.failed("请选择数据来源后再导入");
|
||||
}
|
||||
int max = limit == null || limit <= 0 ? properties.getAutoImportBatchSize() : limit;
|
||||
int max = limit == null || limit <= 0 ? props().getAutoImportBatchSize() : limit;
|
||||
max = Math.min(Math.max(max, 1), 5000);
|
||||
List<Map<String, Object>> batch = new ArrayList<>();
|
||||
// 不用 SET.scan(Redisson/Spring Data Redis 3.3 Cursor 不兼容)
|
||||
@@ -1174,6 +1213,17 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渠道匹配 SQL:商游兼容历史 sheet_name 为空;其它渠道精确匹配。
|
||||
* 占位符 {0} 由 Wrapper.apply 传入 channel。
|
||||
*/
|
||||
private static String channelSheetSql(String channel) {
|
||||
if (SmHotelDataSource.SHANGYOU.equalsIgnoreCase(channel)) {
|
||||
return "(sheet_name is null or sheet_name = '' or lower(sheet_name) = {0})";
|
||||
}
|
||||
return "lower(sheet_name) = {0}";
|
||||
}
|
||||
|
||||
/** 人工同步必须显式传;定时任务用配置 defaultImportDataSource */
|
||||
private String resolveImportDataSource(String dataSource, String triggerType) {
|
||||
String source = StrUtil.trim(dataSource);
|
||||
@@ -1181,7 +1231,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
return source;
|
||||
}
|
||||
if (!"MANUAL".equalsIgnoreCase(StrUtil.trim(triggerType))) {
|
||||
return StrUtil.trim(properties.getDefaultImportDataSource());
|
||||
return StrUtil.trim(props().getDefaultImportDataSource());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1189,15 +1239,15 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
@Override
|
||||
public Map<String, Object> getSyncSchedule() {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("springScheduleEnabled", properties.isSpringScheduleEnabled());
|
||||
m.put("hotelListDailySyncEnabled", properties.isHotelListDailySyncEnabled());
|
||||
m.put("hotelListDailyCron", properties.getHotelListDailyCron());
|
||||
m.put("priceRollDailyEnabled", properties.isPriceRollDailyEnabled());
|
||||
m.put("priceRollDailyCron", properties.getPriceRollDailyCron());
|
||||
m.put("localDailyRollEnabled", properties.isLocalDailyRollEnabled());
|
||||
m.put("localDailyRollCron", properties.getLocalDailyRollCron());
|
||||
m.put("autoImportNewHotels", properties.isAutoImportNewHotels());
|
||||
m.put("autoMarkOfflineHotels", properties.isAutoMarkOfflineHotels());
|
||||
m.put("springScheduleEnabled", props().isSpringScheduleEnabled());
|
||||
m.put("hotelListDailySyncEnabled", props().isHotelListDailySyncEnabled());
|
||||
m.put("hotelListDailyCron", props().getHotelListDailyCron());
|
||||
m.put("priceRollDailyEnabled", props().isPriceRollDailyEnabled());
|
||||
m.put("priceRollDailyCron", props().getPriceRollDailyCron());
|
||||
m.put("localDailyRollEnabled", props().isLocalDailyRollEnabled());
|
||||
m.put("localDailyRollCron", props().getLocalDailyRollCron());
|
||||
m.put("autoImportNewHotels", props().isAutoImportNewHotels());
|
||||
m.put("autoMarkOfflineHotels", props().isAutoMarkOfflineHotels());
|
||||
m.put("hint", "改 Nacos/本地 shangyou-hotel.hotel-list-daily-cron、price-roll-daily-cron、local-daily-roll-cron;"
|
||||
+ "若用 XXL 请设 spring-schedule-enabled=false,在 XXL 控制台配 Cron(含 smHotelLocalDailyRollJobHandler)");
|
||||
return m;
|
||||
@@ -1210,13 +1260,13 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> jobSyncHotelList(String triggerType) {
|
||||
Long tenantId = properties.getCallbackTenantId() == null ? 1L : properties.getCallbackTenantId();
|
||||
Long tenantId = props().getCallbackTenantId() == null ? 1L : props().getCallbackTenantId();
|
||||
return TenantBroker.applyAs(tenantId, tid -> {
|
||||
if (!properties.isHotelListDailySyncEnabled()) {
|
||||
if (!props().isHotelListDailySyncEnabled()) {
|
||||
return Map.of("skipped", true, "reason", "hotel-list-daily-sync-enabled=false");
|
||||
}
|
||||
R<SmHotelSyncLogEntity> r = startListSyncAsync(properties.isAutoImportNewHotels(),
|
||||
properties.getDefaultImportDataSource(), StrUtil.blankToDefault(triggerType, "XXL"));
|
||||
R<SmHotelSyncLogEntity> r = startListSyncAsync(props().isAutoImportNewHotels(),
|
||||
props().getDefaultImportDataSource(), StrUtil.blankToDefault(triggerType, "XXL"));
|
||||
if (r.getCode() != 0) {
|
||||
return Map.of("ok", false, "msg", r.getMsg());
|
||||
}
|
||||
@@ -1231,9 +1281,9 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> jobRollDailyPrice(String triggerType) {
|
||||
Long tenantId = properties.getCallbackTenantId() == null ? 1L : properties.getCallbackTenantId();
|
||||
Long tenantId = props().getCallbackTenantId() == null ? 1L : props().getCallbackTenantId();
|
||||
return TenantBroker.applyAs(tenantId, tid -> {
|
||||
if (!properties.isPriceRollDailyEnabled()) {
|
||||
if (!props().isPriceRollDailyEnabled()) {
|
||||
return Map.of("skipped", true, "reason", "price-roll-daily-enabled=false");
|
||||
}
|
||||
R<SmHotelSyncLogEntity> r = startPriceRollAsync(StrUtil.blankToDefault(triggerType, "XXL"));
|
||||
@@ -1251,9 +1301,9 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> jobRollLocalDaily(String triggerType) {
|
||||
Long tenantId = properties.getCallbackTenantId() == null ? 1L : properties.getCallbackTenantId();
|
||||
Long tenantId = props().getCallbackTenantId() == null ? 1L : props().getCallbackTenantId();
|
||||
return TenantBroker.applyAs(tenantId, tid -> {
|
||||
if (!properties.isLocalDailyRollEnabled()) {
|
||||
if (!props().isLocalDailyRollEnabled()) {
|
||||
return Map.of("skipped", true, "reason", "local-daily-roll-enabled=false");
|
||||
}
|
||||
R<SmHotelSyncLogEntity> r = startLocalDailyRollAsync(StrUtil.blankToDefault(triggerType, "XXL"));
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package com.bsd.pjcloud.supermarket.util;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
|
||||
/**
|
||||
* 行政区划 adcode 前缀匹配(与 App 列表筛选一致):
|
||||
* 选省/市可覆盖全部下级区县。
|
||||
*/
|
||||
public final class HotelAreaAdcodeUtil {
|
||||
|
||||
private HotelAreaAdcodeUtil() {
|
||||
}
|
||||
|
||||
/** 330000→33,330100→3301,330102→330102 */
|
||||
public static String areaAdcodePrefix(long adcode) {
|
||||
String s = String.valueOf(adcode);
|
||||
if (s.length() > 6) {
|
||||
return s.substring(0, 6);
|
||||
}
|
||||
if (s.length() == 6 && s.endsWith("0000")) {
|
||||
return s.substring(0, 2);
|
||||
}
|
||||
if (s.length() == 6 && s.endsWith("00")) {
|
||||
return s.substring(0, 4);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* 酒店是否落在授权区域范围内。
|
||||
*
|
||||
* @param scopeAdcode 映射表配置的区域;null 表示不限区域
|
||||
* @param scopeName 映射表区域名(名称兜底)
|
||||
* @param hotelAdcode 酒店 districtAdcode
|
||||
* @param hotelDistrict 酒店 district 名称
|
||||
*/
|
||||
public static boolean matchRegion(Long scopeAdcode, String scopeName, Long hotelAdcode, String hotelDistrict) {
|
||||
if (scopeAdcode == null) {
|
||||
return true;
|
||||
}
|
||||
if (hotelAdcode != null) {
|
||||
String prefix = areaAdcodePrefix(scopeAdcode);
|
||||
return String.valueOf(hotelAdcode).startsWith(prefix);
|
||||
}
|
||||
// 酒店未落 adcode:按区域名兜底
|
||||
String name = StrUtil.trim(scopeName);
|
||||
if (StrUtil.isBlank(name)) {
|
||||
return false;
|
||||
}
|
||||
String hotelName = StrUtil.trim(hotelDistrict);
|
||||
if (StrUtil.isBlank(hotelName)) {
|
||||
return false;
|
||||
}
|
||||
if (hotelName.contains(name) || name.contains(hotelName)) {
|
||||
return true;
|
||||
}
|
||||
String shortName = shortenAreaName(name);
|
||||
return StrUtil.isNotBlank(shortName) && hotelName.contains(shortName);
|
||||
}
|
||||
|
||||
public static String shortenAreaName(String name) {
|
||||
if (StrUtil.isBlank(name)) {
|
||||
return "";
|
||||
}
|
||||
String n = name.trim();
|
||||
n = n.replaceAll("(壮族|回族|维吾尔|藏族|彝族|朝鲜族)?自治区$", "");
|
||||
n = n.replaceAll("(特别行政区|省|市|区|县|旗|盟)$", "");
|
||||
return n.trim();
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package com.bsd.pjcloud.supermarket.util;
|
||||
|
||||
/**
|
||||
* 去掉 JSON 中的行注释与块注释(字符串字面量内保留,避免误伤 https 地址)。
|
||||
* 便于后台渠道对接配置带中文说明保存。
|
||||
*/
|
||||
public final class JsonCommentUtil {
|
||||
|
||||
private JsonCommentUtil() {
|
||||
}
|
||||
|
||||
public static String strip(String input) {
|
||||
if (input == null || input.isEmpty()) {
|
||||
return input;
|
||||
}
|
||||
StringBuilder out = new StringBuilder(input.length());
|
||||
boolean inString = false;
|
||||
boolean escape = false;
|
||||
boolean inLineComment = false;
|
||||
boolean inBlockComment = false;
|
||||
for (int i = 0; i < input.length(); i++) {
|
||||
char c = input.charAt(i);
|
||||
char next = i + 1 < input.length() ? input.charAt(i + 1) : 0;
|
||||
if (inLineComment) {
|
||||
if (c == '\n') {
|
||||
inLineComment = false;
|
||||
out.append(c);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (inBlockComment) {
|
||||
if (c == '*' && next == '/') {
|
||||
inBlockComment = false;
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (inString) {
|
||||
out.append(c);
|
||||
if (escape) {
|
||||
escape = false;
|
||||
}
|
||||
else if (c == '\\') {
|
||||
escape = true;
|
||||
}
|
||||
else if (c == '"') {
|
||||
inString = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c == '"') {
|
||||
inString = true;
|
||||
out.append(c);
|
||||
continue;
|
||||
}
|
||||
if (c == '/' && next == '/') {
|
||||
inLineComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c == '/' && next == '*') {
|
||||
inBlockComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
out.append(c);
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
}
|
||||
@@ -110,18 +110,13 @@ spring:
|
||||
# # 同步后:已停用但上游又出现 → 自动恢复 ACTIVE
|
||||
# auto-reactivate-online-hotels: true
|
||||
#
|
||||
## ========== 下游销售方推送(《第三方数据同步接口文档》)==========
|
||||
## 本系统作为中转 A,主动把加价后的酒店/房价推到下游 B(与上面「上游」无关,可单独开关)
|
||||
## ========== 下游销售方推送(环启等;优先走库表「渠道对接配置」direction=downstream)==========
|
||||
## 本系统作为 A,主动 POST 到 B 的 /third/party/api/*;可配置多家;部门范围见「渠道部门映射」
|
||||
## 下列 YAML 仅作无库表时的单下游兼容回落(partner=default)
|
||||
#hotel-downstream:
|
||||
# # 是否启用向下游推送
|
||||
# enabled: false
|
||||
# # 下游 B 系统根地址(不含尾斜杠),实际请求 = base-url + /third/party/api/...
|
||||
# base-url: https://b.example.com
|
||||
# # B 系统分配给我们的 appKey(请求头/签名用)
|
||||
# base-url: http://114.55.58.87:8080/prod-api
|
||||
# app-key: TP_xxx
|
||||
# # B 系统分配的 appSecret(仅本地算签名,不落报文)
|
||||
# app-secret: your-secret
|
||||
# # 推下游 HTTP 超时(毫秒)
|
||||
# timeout-ms: 30000
|
||||
# # 兼容字段;实际上游拉取天数以 shangyou-hotel.price-days 为准
|
||||
# daily-days: 90
|
||||
Reference in new issue
Block a user