酒店
This commit is contained in:
1 parent
2c02fb14e8
commit
ae8d5d0835
11 files changed
+294
-37
No files matched your search
+3
@@ -56,6 +56,9 @@ public class SmHotelDownstreamChannelConfigDTO {
|
||||
@Schema(description = "product/daily 每分钟上限,默认 300;≤0 关闭")
|
||||
private Integer rateLimitProductDailyPerMin;
|
||||
|
||||
@Schema(description = "慧订 hoteldatepush 出站 QPS,默认 5;≤0 关闭")
|
||||
private Integer rateLimitHotelDatePushQps;
|
||||
|
||||
@Schema(description = "order/query 每分钟上限,默认 120;≤0 关闭")
|
||||
private Integer rateLimitOrderQueryPerMin;
|
||||
|
||||
|
||||
+6
-1
@@ -129,11 +129,16 @@ public class SmHotelOrderEntity extends Model<SmHotelOrderEntity> {
|
||||
@Schema(description = "酒店名称")
|
||||
private String hotelName;
|
||||
|
||||
/** 详情展示:产品名称(非表字段) */
|
||||
/** 列表/详情展示:产品名称(非表字段) */
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "产品名称")
|
||||
private String productName;
|
||||
|
||||
/** 列表展示:房型名称(非表字段) */
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "房型名称")
|
||||
private String roomTypeName;
|
||||
|
||||
/** 列表展示:下单来源 APP/ADMIN/LOCAL/DOWNSTREAM(非表字段) */
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "下单来源")
|
||||
|
||||
+5
@@ -77,4 +77,9 @@ public class SmHotelProductEntity extends Model<SmHotelProductEntity> {
|
||||
@TableLogic
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String delFlag;
|
||||
|
||||
/** App 详情:所选入住区间无日历/不可订时为 true(非表字段) */
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "所选日期已售罄")
|
||||
private Boolean soldOut;
|
||||
}
|
||||
+6
@@ -70,6 +70,12 @@ public class HotelDownstreamProperties {
|
||||
/** /product/sync、/daily/sync 每分钟上限(环启文档 300) */
|
||||
private int rateLimitProductDailyPerMin = 300;
|
||||
|
||||
/**
|
||||
* 慧订 /splider/hoteldatepush 出站 QPS(对方已限流)。
|
||||
* 默认 5;≤0 关闭自我节流。多实例请按实例均分,例如 2 实例各配 2~3。
|
||||
*/
|
||||
private int rateLimitHotelDatePushQps = 5;
|
||||
|
||||
/** /order/query 每分钟上限(环启文档 120) */
|
||||
private int rateLimitOrderQueryPerMin = 120;
|
||||
|
||||
|
||||
+70
-30
@@ -87,7 +87,7 @@ public class SmHotelAppController {
|
||||
private static final String CANDIDATE_CACHE_OVERFLOW = "OVERFLOW";
|
||||
|
||||
/** 列表页结果短缓存(同条件重复进入/下拉) */
|
||||
private static final String PAGE_CACHE_PREFIX = "sm:hotel:app:page:v5:";
|
||||
private static final String PAGE_CACHE_PREFIX = "sm:hotel:app:page:v6:";
|
||||
|
||||
private static final long PAGE_CACHE_SECONDS = 45L;
|
||||
|
||||
@@ -271,18 +271,23 @@ public class SmHotelAppController {
|
||||
int candSize = -1;
|
||||
boolean smallScope = scopedIds != null && !scopedIds.isEmpty() && scopedIds.size() <= MAX_SCOPED_HOTEL_IDS;
|
||||
if (smallScope) {
|
||||
List<Long> candidateIds = resolveAppListCandidateHotelIds(stayFrom, stayToExclusive, needCap, scopedIds);
|
||||
candSize = candidateIds == null ? -1 : candidateIds.size();
|
||||
if (candidateIds != null && candidateIds.isEmpty()) {
|
||||
return R.ok(emptyPage(page));
|
||||
}
|
||||
if (candidateIds != null) {
|
||||
wrapper.in(SmHotelEntity::getId, candidateIds);
|
||||
List<Long> candidateIds = scopedIds;
|
||||
if (needCap > 1) {
|
||||
List<Long> capHotels = roomMapper.selectDistinctHotelIdsByMinCapacity(needCap);
|
||||
if (capHotels == null || capHotels.isEmpty()) {
|
||||
return R.ok(emptyPage(page));
|
||||
}
|
||||
Set<Long> capSet = new java.util.HashSet<>(capHotels);
|
||||
candidateIds = scopedIds.stream().filter(capSet::contains).toList();
|
||||
if (candidateIds.isEmpty()) {
|
||||
return R.ok(emptyPage(page));
|
||||
}
|
||||
}
|
||||
candSize = candidateIds.size();
|
||||
wrapper.in(SmHotelEntity::getId, candidateIds);
|
||||
}
|
||||
else {
|
||||
// 全国:按评分分页 + 每家酒店 EXISTS 可订,不做扫描上限(可订的都会出现,只是排序靠后)
|
||||
applyBookableHotelFilter(wrapper, stayFrom, stayToExclusive);
|
||||
// 全国:上架酒店都出列表;所选日期无日历的在行上标已售罄,不再按可订日历过滤掉
|
||||
applyGuestCapacityFilter(wrapper, needCap);
|
||||
}
|
||||
if (minPrice != null || maxPrice != null) {
|
||||
@@ -297,7 +302,12 @@ public class SmHotelAppController {
|
||||
.orderByDesc(SmHotelEntity::getId);
|
||||
raw = pageWithoutCount(page, wrapper);
|
||||
List<Long> ids = raw.getRecords().stream().map(SmHotelEntity::getId).filter(Objects::nonNull).toList();
|
||||
Map<Long, BigDecimal> minPriceMap = loadMinSellPrice(ids);
|
||||
LocalDate priceFrom = stayFrom != null ? stayFrom : LocalDate.now();
|
||||
LocalDate priceToEx = stayToExclusive;
|
||||
if (priceToEx == null || !priceToEx.isAfter(priceFrom)) {
|
||||
priceToEx = priceFrom.plusDays(1);
|
||||
}
|
||||
Map<Long, BigDecimal> minPriceMap = loadMinSellPrice(ids, priceFrom, priceToEx);
|
||||
Page<Map<String, Object>> out = new Page<>(raw.getCurrent(), raw.getSize(), raw.getTotal());
|
||||
out.setRecords(raw.getRecords().stream()
|
||||
.filter(Objects::nonNull)
|
||||
@@ -454,7 +464,10 @@ public class SmHotelAppController {
|
||||
|
||||
@Operation(summary = "酒店详情(含房型/产品)")
|
||||
@GetMapping("/detail/{id}")
|
||||
public R<SmHotelDetailVO> detail(@PathVariable Long id) {
|
||||
public R<SmHotelDetailVO> detail(@PathVariable Long id,
|
||||
@RequestParam(required = false) String checkIn,
|
||||
@RequestParam(required = false) String checkOut,
|
||||
@RequestParam(required = false) Integer rooms) {
|
||||
R<SmHotelDetailVO> r = midService.getHotelDetail(id);
|
||||
if (r.getCode() != 0 || r.getData() == null || r.getData().getHotel() == null) {
|
||||
return r;
|
||||
@@ -462,31 +475,56 @@ public class SmHotelAppController {
|
||||
if ("OFFLINE".equalsIgnoreCase(r.getData().getHotel().getUpstreamStatus())) {
|
||||
return R.failed("酒店已下架");
|
||||
}
|
||||
// App 侧:没有未来日历的产品无法预订,不展示
|
||||
hideProductsWithoutCalendar(r.getData());
|
||||
// 无日历的产品仍展示,所选入住日标已售罄
|
||||
markProductSoldOut(r.getData(), parseDay(checkIn), parseDay(checkOut), rooms);
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤无未来日历的产品。无日历则无法计价/占库存,列表展示无意义。
|
||||
* 按入住半开区间给产品打已售罄:缺日历、关房或剩余库存不足。
|
||||
*/
|
||||
private void hideProductsWithoutCalendar(SmHotelDetailVO vo) {
|
||||
private void markProductSoldOut(SmHotelDetailVO vo, LocalDate checkIn, LocalDate checkOut, Integer rooms) {
|
||||
if (vo == null || vo.getHotel() == null || vo.getHotel().getId() == null
|
||||
|| vo.getProducts() == null || vo.getProducts().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<SmHotelDailyEntity> rows = Db.lambdaQuery(SmHotelDailyEntity.class)
|
||||
LocalDate from = checkIn != null ? checkIn : LocalDate.now();
|
||||
LocalDate toEx = checkOut;
|
||||
if (toEx == null || !toEx.isAfter(from)) {
|
||||
toEx = from.plusDays(1);
|
||||
}
|
||||
int need = rooms == null || rooms < 1 ? 1 : Math.min(rooms, 20);
|
||||
List<LocalDate> nights = new ArrayList<>();
|
||||
for (LocalDate d = from; d.isBefore(toEx); d = d.plusDays(1)) {
|
||||
nights.add(d);
|
||||
}
|
||||
List<SmHotelDailyEntity> rows = dailyMapper.selectList(Wrappers.<SmHotelDailyEntity>lambdaQuery()
|
||||
.eq(SmHotelDailyEntity::getHotelId, vo.getHotel().getId())
|
||||
.ge(SmHotelDailyEntity::getStayDate, LocalDate.now())
|
||||
.select(SmHotelDailyEntity::getProductCode)
|
||||
.list();
|
||||
Set<String> withDaily = rows.stream()
|
||||
.map(SmHotelDailyEntity::getProductCode)
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
vo.setProducts(vo.getProducts().stream()
|
||||
.filter(p -> p != null && withDaily.contains(p.getProductCode()))
|
||||
.collect(Collectors.toList()));
|
||||
.ge(SmHotelDailyEntity::getStayDate, from)
|
||||
.lt(SmHotelDailyEntity::getStayDate, toEx));
|
||||
Map<String, Set<LocalDate>> bookable = new HashMap<>();
|
||||
if (rows != null) {
|
||||
for (SmHotelDailyEntity row : rows) {
|
||||
if (row == null || StrUtil.isBlank(row.getProductCode()) || row.getStayDate() == null) {
|
||||
continue;
|
||||
}
|
||||
int status = row.getDailyStatus() == null ? 1 : row.getDailyStatus();
|
||||
int stock = row.getStock() == null ? 0 : row.getStock();
|
||||
int sold = row.getSold() == null ? 0 : row.getSold();
|
||||
if (status != 1 || stock - sold < need) {
|
||||
continue;
|
||||
}
|
||||
bookable.computeIfAbsent(row.getProductCode(), k -> new java.util.HashSet<>()).add(row.getStayDate());
|
||||
}
|
||||
}
|
||||
for (SmHotelProductEntity p : vo.getProducts()) {
|
||||
if (p == null) {
|
||||
continue;
|
||||
}
|
||||
Set<LocalDate> ok = bookable.getOrDefault(p.getProductCode(), Set.of());
|
||||
boolean soldOut = nights.isEmpty() || nights.stream().anyMatch(d -> !ok.contains(d));
|
||||
p.setSoldOut(soldOut);
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "酒店日历分页")
|
||||
@@ -698,15 +736,17 @@ public class SmHotelAppController {
|
||||
row.put("phone", h.getPhone());
|
||||
row.put("sheetName", h.getSheetName());
|
||||
row.put("minSellPrice", minSellPrice);
|
||||
row.put("soldOut", minSellPrice == null);
|
||||
return row;
|
||||
}
|
||||
|
||||
private Map<Long, BigDecimal> loadMinSellPrice(List<Long> hotelIds) {
|
||||
/** 所选入住区间可订日历的最低卖价;没有可订日历则不返回,列表标已售罄 */
|
||||
private Map<Long, BigDecimal> loadMinSellPrice(List<Long> hotelIds, LocalDate from, LocalDate toExclusive) {
|
||||
Map<Long, BigDecimal> map = new HashMap<>();
|
||||
if (hotelIds == null || hotelIds.isEmpty()) {
|
||||
if (hotelIds == null || hotelIds.isEmpty() || from == null || toExclusive == null) {
|
||||
return map;
|
||||
}
|
||||
List<Map<String, Object>> rows = productMapper.selectMinSellPriceByHotelIds(hotelIds);
|
||||
List<Map<String, Object>> rows = dailyMapper.selectMinSellPriceByStayRange(from, toExclusive, hotelIds);
|
||||
if (rows == null) {
|
||||
return map;
|
||||
}
|
||||
|
||||
+101
-1
@@ -14,8 +14,11 @@ import com.bsd.pjcloud.supermarket.integration.downstream.DownstreamPushLogConte
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelDownstreamPushLogService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 慧订下游:静态 pushStaticHotels + 价态 hoteldatepush(窗口全量覆盖)。
|
||||
* 请求体带 key / supplierId,成功码为 0(与环启 200 不同)。
|
||||
@@ -27,8 +30,15 @@ public class HuidingDownstreamHotelAdapter implements DownstreamHotelAdapter {
|
||||
|
||||
private static final String TAG = "下游推送[慧订]";
|
||||
|
||||
private static final String RATE_KEY = "sm:hotel:huiding:qps:";
|
||||
|
||||
private final SmHotelDownstreamPushLogService pushLogService;
|
||||
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
|
||||
/** 本进程 hoteldatepush 下次可发时间 */
|
||||
private volatile long datePushNextPermitAtMs = 0L;
|
||||
|
||||
@Override
|
||||
public String protocol() {
|
||||
return SmHotelDownstreamProtocol.HUIDING;
|
||||
@@ -106,6 +116,30 @@ public class HuidingDownstreamHotelAdapter implements DownstreamHotelAdapter {
|
||||
String action, String path, Object bodyObj) {
|
||||
assertReady(partnerCode, props);
|
||||
String bodyJson = bodyObj instanceof String ? (String) bodyObj : JSONUtil.toJsonStr(bodyObj);
|
||||
int maxRetry = Math.max(0, props.getRateLimitMaxRetries());
|
||||
long backoff = Math.max(1000L, props.getRateLimitBackoffMs());
|
||||
CheckedException last = null;
|
||||
for (int attempt = 0; attempt <= maxRetry; attempt++) {
|
||||
acquireHotelDatePushPermit(partnerCode, props, path);
|
||||
try {
|
||||
return doPostOnce(partnerCode, props, hotelId, extHotelId, action, path, bodyJson);
|
||||
}
|
||||
catch (CheckedException e) {
|
||||
last = e;
|
||||
if (!isRateLimitedMsg(e.getMessage()) || attempt >= maxRetry) {
|
||||
throw e;
|
||||
}
|
||||
long wait = backoff * (1L << attempt);
|
||||
log.warn("|{}|限流退避|partner={}|path={}|attempt={}|waitMs={}|", TAG, partnerCode, path, attempt + 1,
|
||||
wait);
|
||||
sleepQuiet(wait);
|
||||
}
|
||||
}
|
||||
throw last != null ? last : new CheckedException("慧订调用失败");
|
||||
}
|
||||
|
||||
private JSONObject doPostOnce(String partnerCode, HotelDownstreamProperties props, Long hotelId, String extHotelId,
|
||||
String action, String path, String bodyJson) {
|
||||
String url = trimSlash(props.getBaseUrl()) + path;
|
||||
long t0 = System.currentTimeMillis();
|
||||
Integer httpStatus = null;
|
||||
@@ -139,7 +173,10 @@ public class HuidingDownstreamHotelAdapter implements DownstreamHotelAdapter {
|
||||
StrUtil.maxLength(trimmed, 180));
|
||||
}
|
||||
}
|
||||
if (json != null) {
|
||||
if (httpStatus != null && httpStatus == 429) {
|
||||
errMsg = StrUtil.format("慧订限流 HTTP 429 [{}] body={}", path, StrUtil.maxLength(trimmed, 180));
|
||||
}
|
||||
else if (json != null) {
|
||||
// 慧订:code=0 成功;兼容部分接口用 200
|
||||
int bizCode = json.getInt("code", -1);
|
||||
if (bizCode == 0 || bizCode == 200) {
|
||||
@@ -214,6 +251,69 @@ public class HuidingDownstreamHotelAdapter implements DownstreamHotelAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* hoteldatepush 出站 QPS:本进程匀速 + Redis 秒窗(多实例共享)。
|
||||
* 配置 rate-limit-hotel-date-push-qps ≤0 关闭。
|
||||
*/
|
||||
private void acquireHotelDatePushPermit(String partnerCode, HotelDownstreamProperties props, String path) {
|
||||
if (!isHotelDatePushPath(path)) {
|
||||
return;
|
||||
}
|
||||
int qps = props.getRateLimitHotelDatePushQps();
|
||||
if (qps <= 0) {
|
||||
return;
|
||||
}
|
||||
long intervalMs = Math.max(1L, Math.round(1000.0 / qps));
|
||||
synchronized (this) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (datePushNextPermitAtMs > now) {
|
||||
sleepQuiet(datePushNextPermitAtMs - now);
|
||||
now = System.currentTimeMillis();
|
||||
}
|
||||
datePushNextPermitAtMs = Math.max(now, datePushNextPermitAtMs) + intervalMs;
|
||||
}
|
||||
int maxPerSec = Math.max(1, qps);
|
||||
String partner = StrUtil.blankToDefault(partnerCode, "default");
|
||||
for (int i = 0; i < 30; i++) {
|
||||
long sec = System.currentTimeMillis() / 1000L;
|
||||
String key = RATE_KEY + partner + ":" + sec;
|
||||
Long n = redisTemplate.opsForValue().increment(key);
|
||||
if (n != null && n == 1L) {
|
||||
redisTemplate.expire(key, 3, TimeUnit.SECONDS);
|
||||
}
|
||||
if (n == null || n <= maxPerSec) {
|
||||
return;
|
||||
}
|
||||
long wait = 1000L - (System.currentTimeMillis() % 1000L) + 5L;
|
||||
sleepQuiet(wait);
|
||||
}
|
||||
log.warn("|{}|hoteldatepush QPS 等待超时仍继续|partner={}|qps={}|", TAG, partner, qps);
|
||||
}
|
||||
|
||||
private static boolean isHotelDatePushPath(String path) {
|
||||
return StrUtil.isNotBlank(path) && path.contains("hoteldatepush");
|
||||
}
|
||||
|
||||
private static boolean isRateLimitedMsg(String msg) {
|
||||
if (StrUtil.isBlank(msg)) {
|
||||
return false;
|
||||
}
|
||||
return msg.contains("过于频繁") || msg.contains("限流") || msg.contains("429")
|
||||
|| msg.toLowerCase().contains("too many") || msg.toLowerCase().contains("rate limit");
|
||||
}
|
||||
|
||||
private static void sleepQuiet(long ms) {
|
||||
if (ms <= 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(ms);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
public static String resolveKey(HotelDownstreamProperties props) {
|
||||
if (props == null) {
|
||||
return "";
|
||||
|
||||
+21
@@ -58,6 +58,27 @@ public interface SmHotelDailyMapper extends PjcloudBaseMapper<SmHotelDailyEntity
|
||||
List<Long> selectDistinctHotelIdsByStayRangeInHotels(@Param("fromDate") LocalDate fromDate,
|
||||
@Param("toExclusive") LocalDate toExclusive, @Param("hotelIds") Collection<Long> hotelIds);
|
||||
|
||||
/**
|
||||
* 入住半开区间内可订日历的最低卖价(开房且有剩余库存)。
|
||||
*/
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT hotel_id AS hotelId, MIN(sell_price) AS minSell
|
||||
FROM canteen_hotel_daily
|
||||
WHERE del_flag = '0'
|
||||
AND stay_date >= #{fromDate}
|
||||
AND stay_date < #{toExclusive}
|
||||
AND IFNULL(daily_status, 1) = 1
|
||||
AND IFNULL(stock, 0) - IFNULL(sold, 0) > 0
|
||||
AND sell_price IS NOT NULL
|
||||
AND hotel_id IN
|
||||
<foreach collection="hotelIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
GROUP BY hotel_id
|
||||
</script>
|
||||
""")
|
||||
List<Map<String, Object>> selectMinSellPriceByStayRange(@Param("fromDate") LocalDate fromDate,
|
||||
@Param("toExclusive") LocalDate toExclusive, @Param("hotelIds") Collection<Long> hotelIds);
|
||||
|
||||
int insertBatch(@Param("list") List<SmHotelDailyEntity> list);
|
||||
|
||||
/**
|
||||
|
||||
+3
@@ -344,6 +344,9 @@ public class SmHotelChannelConfigServiceImpl extends ServiceImpl<SmHotelChannelC
|
||||
if (dto.getRateLimitProductDailyPerMin() != null) {
|
||||
target.setRateLimitProductDailyPerMin(dto.getRateLimitProductDailyPerMin());
|
||||
}
|
||||
if (dto.getRateLimitHotelDatePushQps() != null) {
|
||||
target.setRateLimitHotelDatePushQps(dto.getRateLimitHotelDatePushQps());
|
||||
}
|
||||
if (dto.getRateLimitOrderQueryPerMin() != null) {
|
||||
target.setRateLimitOrderQueryPerMin(dto.getRateLimitOrderQueryPerMin());
|
||||
}
|
||||
|
||||
+1
@@ -913,6 +913,7 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
String protocol = props == null ? null : props.getProtocol();
|
||||
String protocolLabel = SmHotelDownstreamProtocol.label(protocol);
|
||||
row.put("protocol", SmHotelDownstreamProtocol.normalize(protocol));
|
||||
row.put("orderPullEnabled", props != null && props.isOrderPullEnabled());
|
||||
SmHotelChannelConfigEntity cfg = channelConfigService.getByChannel(code,
|
||||
com.bsd.pjcloud.supermarket.constant.SmHotelChannelDirection.DOWNSTREAM);
|
||||
String remark = cfg == null ? null : StrUtil.trim(cfg.getRemark());
|
||||
|
||||
+73
-1
@@ -210,7 +210,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页列表展示字段:酒店名称、数据来源、订单来源(App/后台/分销伙伴协议+编码/下游应用)。
|
||||
* 分页列表展示字段:酒店名称、房型/产品名称、数据来源、订单来源(App/后台/分销伙伴协议+编码/下游应用)。
|
||||
* 酒店数据来源 ≠ 订单销售来源:同一上游(如飞猪)可被多家下游售出,靠 partnerCode / downstreamAppId 区分。
|
||||
*/
|
||||
private void fillOrderListDisplay(List<SmHotelOrderEntity> records) {
|
||||
@@ -290,6 +290,51 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
indexOpenAppNames(partnerNameMap, partnerProtocolMap, app);
|
||||
}
|
||||
}
|
||||
Map<String, SmHotelProductEntity> productByHotelCode = new HashMap<>();
|
||||
Map<String, SmHotelProductEntity> productByCode = new HashMap<>();
|
||||
List<String> productCodes = records.stream()
|
||||
.map(SmHotelOrderEntity::getProductCode)
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (!productCodes.isEmpty()) {
|
||||
var productQ = Wrappers.<SmHotelProductEntity>lambdaQuery()
|
||||
.in(SmHotelProductEntity::getProductCode, productCodes);
|
||||
if (!hotelIds.isEmpty()) {
|
||||
productQ.in(SmHotelProductEntity::getHotelId, hotelIds);
|
||||
}
|
||||
for (SmHotelProductEntity p : productMapper.selectList(productQ)) {
|
||||
if (StrUtil.isNotBlank(p.getProductCode())) {
|
||||
productByCode.putIfAbsent(p.getProductCode(), p);
|
||||
if (p.getHotelId() != null) {
|
||||
productByHotelCode.put(p.getHotelId() + "|" + p.getProductCode(), p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Map<String, SmHotelRoomEntity> roomByHotelCode = new HashMap<>();
|
||||
List<String> roomTypeCodes = new ArrayList<>(records.stream()
|
||||
.map(SmHotelOrderEntity::getRoomTypeCode)
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.distinct()
|
||||
.collect(Collectors.toList()));
|
||||
for (SmHotelProductEntity p : productByCode.values()) {
|
||||
if (StrUtil.isNotBlank(p.getRoomTypeCode()) && !roomTypeCodes.contains(p.getRoomTypeCode())) {
|
||||
roomTypeCodes.add(p.getRoomTypeCode());
|
||||
}
|
||||
}
|
||||
if (!roomTypeCodes.isEmpty()) {
|
||||
var roomQ = Wrappers.<SmHotelRoomEntity>lambdaQuery()
|
||||
.in(SmHotelRoomEntity::getRoomTypeCode, roomTypeCodes);
|
||||
if (!hotelIds.isEmpty()) {
|
||||
roomQ.in(SmHotelRoomEntity::getHotelId, hotelIds);
|
||||
}
|
||||
for (SmHotelRoomEntity r : roomMapper.selectList(roomQ)) {
|
||||
if (r.getHotelId() != null && StrUtil.isNotBlank(r.getRoomTypeCode())) {
|
||||
roomByHotelCode.putIfAbsent(r.getHotelId() + "|" + r.getRoomTypeCode(), r);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (SmHotelOrderEntity o : records) {
|
||||
SmHotelEntity hotel = o.getHotelId() != null ? hotelById.get(o.getHotelId()) : null;
|
||||
if (hotel == null && StrUtil.isNotBlank(o.getHotelCode())) {
|
||||
@@ -299,6 +344,28 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
o.setHotelName(hotel.getHotelName());
|
||||
o.setHotelDataSource(upstreamGatewayRegistry.resolveChannelKey(hotel.getSheetName()));
|
||||
}
|
||||
SmHotelProductEntity product = null;
|
||||
if (StrUtil.isNotBlank(o.getProductCode())) {
|
||||
if (o.getHotelId() != null) {
|
||||
product = productByHotelCode.get(o.getHotelId() + "|" + o.getProductCode());
|
||||
}
|
||||
if (product == null) {
|
||||
product = productByCode.get(o.getProductCode());
|
||||
}
|
||||
}
|
||||
if (product != null) {
|
||||
o.setProductName(product.getProductName());
|
||||
}
|
||||
String roomTypeCode = StrUtil.blankToDefault(o.getRoomTypeCode(),
|
||||
product == null ? null : product.getRoomTypeCode());
|
||||
Long roomHotelId = o.getHotelId() != null ? o.getHotelId()
|
||||
: (hotel == null ? null : hotel.getId());
|
||||
if (roomHotelId != null && StrUtil.isNotBlank(roomTypeCode)) {
|
||||
SmHotelRoomEntity room = roomByHotelCode.get(roomHotelId + "|" + roomTypeCode);
|
||||
if (room != null) {
|
||||
o.setRoomTypeName(room.getRoomTypeName());
|
||||
}
|
||||
}
|
||||
SmHotelDownstreamAppEntity app = o.getDownstreamAppId() == null ? null
|
||||
: appById.get(o.getDownstreamAppId());
|
||||
if (app != null) {
|
||||
@@ -2188,6 +2255,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
order.setUpstreamRespJson(JSONUtil.toJsonStr(body));
|
||||
order.setLastCompensateMsg(StrUtil.sub("状态回调更新 bookStatus=" + bookStatus, 0, 500));
|
||||
baseMapper.updateById(order);
|
||||
notifyOpenOrderCallback(order);
|
||||
return Map.of("updated", true, "localOrderId", order.getId(), "orderStatus", order.getOrderStatus());
|
||||
}
|
||||
|
||||
@@ -2304,6 +2372,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
order.setFailMsg(StrUtil.sub(reason, 0, 200));
|
||||
}
|
||||
baseMapper.updateById(order);
|
||||
notifyOpenOrderCallback(order);
|
||||
return R.ok(true, "本地订单已取消并回补库存");
|
||||
}
|
||||
// 尚无上游/本地确认单号:未支付或支付后履约失败等,仅关本地单并回补预占
|
||||
@@ -2319,12 +2388,14 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
order.setFailMsg(StrUtil.sub(reason, 0, 200));
|
||||
}
|
||||
baseMapper.updateById(order);
|
||||
notifyOpenOrderCallback(order);
|
||||
return R.ok(true, PAY_UNPAID.equals(order.getPayStatus()) ? "未支付订单已取消" : "本地订单已关闭");
|
||||
}
|
||||
R<Boolean> cancelR = cancelUpstreamOrder(order, reason);
|
||||
if (cancelR == null || !cancelR.isOk()) {
|
||||
return R.failed(cancelR == null ? "取消失败" : cancelR.getMsg());
|
||||
}
|
||||
notifyOpenOrderCallback(baseMapper.selectById(order.getId()));
|
||||
return R.ok(true);
|
||||
}
|
||||
|
||||
@@ -2413,6 +2484,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
order.setFailMsg(StrUtil.sub(reason, 0, 200));
|
||||
}
|
||||
baseMapper.updateById(order);
|
||||
notifyOpenOrderCallback(order);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -112,10 +112,10 @@ spring:
|
||||
#
|
||||
## ========== 下游开放平台(/third/party/open)==========
|
||||
## 图片公网根地址:相对路径(如 /admin/sys-file/...)会拼此前缀后下发给下游
|
||||
sm-hotel:
|
||||
open:
|
||||
# public-file-base-url: https://www.zianxin.com/api
|
||||
public-file-base-url: http://127.0.0.1:9999
|
||||
#sm-hotel:
|
||||
# open:
|
||||
# # public-file-base-url: https://www.zianxin.com/api
|
||||
# public-file-base-url: http://127.0.0.1:9999
|
||||
|
||||
## ========== 下游销售方推送(环启等;优先走库表「渠道对接配置」direction=downstream)==========
|
||||
## 本系统作为 A,主动 POST 到 B 的 /third/party/api/*;可配置多家;部门范围见「渠道部门映射」
|
||||
@@ -135,6 +135,7 @@ sm-hotel:
|
||||
# # --- 限流(也可写在伙伴 YAML,库表会覆盖这里)---
|
||||
# rate-limit-hotel-apartment-per-min: 600 # hotel+apartment / 分钟
|
||||
# rate-limit-product-daily-per-min: 300 # product+daily / 分钟
|
||||
# rate-limit-hotel-date-push-qps: 5 # 慧订 hoteldatepush 出站 QPS;≤0 关闭
|
||||
# rate-limit-order-query-per-min: 120 # order/query / 分钟
|
||||
# rate-limit-order-detail-per-min: 600 # order/detail / 分钟
|
||||
# rate-limit-max-retries: 3 # 命中「过于频繁」最多再试几次
|
||||
|
||||
Reference in new issue
Block a user