酒店,食堂
This commit is contained in:
1 parent
0865a8836d
commit
bc2fd545f1
24 files changed
+1664
-96
No files matched your search
+18
-1
@@ -78,9 +78,26 @@ public class DailyMenuAdminVO {
|
||||
/** 剩余总库存(与 remainStock 同义,兼容字段) */
|
||||
private Integer remainOfflineStock;
|
||||
|
||||
/** 剩余总库存 */
|
||||
/** 剩余总库存(套餐可能已被组成菜可拼份数收紧) */
|
||||
private Integer remainStock;
|
||||
|
||||
/**
|
||||
* 本行自身剩余:总库存 − 移动端已订 − 消费机已售(套餐取组成菜 min 之前)。
|
||||
* 单品与 remainStock 同值;套餐用于对照「为何剩余被压小」。
|
||||
*/
|
||||
private Integer selfRemainStock;
|
||||
|
||||
/**
|
||||
* 套餐:同日同餐段组成菜可拼出的套餐份数;非套餐或无法计算时为空。
|
||||
*/
|
||||
private Integer comboFormableRemain;
|
||||
|
||||
/** 套餐剩余是否被组成菜可拼份数卡住(remainStock < selfRemainStock) */
|
||||
private Boolean remainLimitedByCombo;
|
||||
|
||||
/** 剩余口径说明,如「本行585,受组成菜限制为28」;未受限可为空 */
|
||||
private String remainStockHint;
|
||||
|
||||
private Integer price;
|
||||
|
||||
private Integer spuType;
|
||||
|
||||
+24
@@ -179,6 +179,11 @@ public class CanteenDailyMenuStockSupport {
|
||||
row.setRemainOfflineStock(shared);
|
||||
row.setStock(shared);
|
||||
row.setRemainStock(shared);
|
||||
// 本行自身剩余(套餐联动前),便于管理端解释「剩余为何小于总库存−已订」
|
||||
row.setSelfRemainStock(shared);
|
||||
row.setComboFormableRemain(null);
|
||||
row.setRemainLimitedByCombo(Boolean.FALSE);
|
||||
row.setRemainStockHint(null);
|
||||
if (row.getSpuId() != null) {
|
||||
byMealSpu.put(mealSpuKey(row.getHourType(), row.getSpuId()), row);
|
||||
}
|
||||
@@ -193,13 +198,32 @@ public class CanteenDailyMenuStockSupport {
|
||||
row.setRemainOnlineStock(Math.min(row.getRemainOnlineStock(), formableOnline));
|
||||
}
|
||||
if (formableOffline >= 0) {
|
||||
row.setComboFormableRemain(formableOffline);
|
||||
row.setRemainOfflineStock(Math.min(row.getRemainOfflineStock(), formableOffline));
|
||||
}
|
||||
row.setStock(row.getRemainOfflineStock());
|
||||
row.setRemainStock(row.getRemainOfflineStock());
|
||||
applyComboRemainHint(row);
|
||||
}
|
||||
}
|
||||
|
||||
/** 套餐剩余被组成菜卡住时写提示文案,列表/详情直接展示 */
|
||||
private static void applyComboRemainHint(DailyMenuAdminVO row) {
|
||||
if (row == null) {
|
||||
return;
|
||||
}
|
||||
int self = nullToZero(row.getSelfRemainStock());
|
||||
int effective = nullToZero(row.getRemainStock());
|
||||
Integer formable = row.getComboFormableRemain();
|
||||
boolean limited = formable != null && effective < self;
|
||||
row.setRemainLimitedByCombo(limited);
|
||||
if (!limited) {
|
||||
row.setRemainStockHint(null);
|
||||
return;
|
||||
}
|
||||
row.setRemainStockHint("本行剩余 " + self + ",受组成菜可拼限制为 " + effective);
|
||||
}
|
||||
|
||||
private static void applyAppQuotasAndRemains(DailyMenuAppVO row, boolean stockAsOnline) {
|
||||
int total = totalStockOf(row.getOfflineStock());
|
||||
int mobile = quotaOrDefault(row.getOnlineStock());
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.bsd.pjcloud.supermarket.constant;
|
||||
|
||||
/**
|
||||
* 酒店下游统一开放 API 路径与限流接口码(对方按我方规范对接)。
|
||||
*/
|
||||
public final class SmHotelOpenApi {
|
||||
|
||||
public static final String BASE = "/third/party/open";
|
||||
|
||||
public static final String HOTEL_LIST = BASE + "/hotel/list";
|
||||
|
||||
public static final String HOTEL_DETAIL = BASE + "/hotel/detail";
|
||||
|
||||
public static final String HOTEL_DAILY = BASE + "/hotel/daily";
|
||||
|
||||
public static final String ORDER_CHECK = BASE + "/order/check";
|
||||
|
||||
public static final String ORDER_CREATE = BASE + "/order/create";
|
||||
|
||||
public static final String ORDER_CANCEL = BASE + "/order/cancel";
|
||||
|
||||
public static final String ORDER_QUERY = BASE + "/order/query";
|
||||
|
||||
/** 限流接口码 */
|
||||
public static final String API_HOTEL_LIST = "hotel.list";
|
||||
|
||||
public static final String API_HOTEL_DETAIL = "hotel.detail";
|
||||
|
||||
public static final String API_HOTEL_DAILY = "hotel.daily";
|
||||
|
||||
public static final String API_ORDER_CHECK = "order.check";
|
||||
|
||||
public static final String API_ORDER_CREATE = "order.create";
|
||||
|
||||
public static final String API_ORDER_CANCEL = "order.cancel";
|
||||
|
||||
public static final String API_ORDER_QUERY = "order.query";
|
||||
|
||||
/** 默认每秒 QPS(可被应用 rate_limit_json 覆盖) */
|
||||
public static final int DEFAULT_QPS = 20;
|
||||
|
||||
private SmHotelOpenApi() {
|
||||
}
|
||||
}
|
||||
+27
-1
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
|
||||
@TenantTable
|
||||
@TableName("canteen_hotel_downstream_app")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "下游销售应用")
|
||||
@Schema(description = "下游开放应用(统一开放平台凭证)")
|
||||
public class SmHotelDownstreamAppEntity extends Model<SmHotelDownstreamAppEntity> {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@@ -21,12 +21,33 @@ public class SmHotelDownstreamAppEntity extends Model<SmHotelDownstreamAppEntity
|
||||
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "下游名称")
|
||||
private String appName;
|
||||
|
||||
@Schema(description = "订单来源伙伴编码(空则用 appKey)")
|
||||
private String partnerCode;
|
||||
|
||||
@Schema(description = "appKey(发给对方)")
|
||||
private String appKey;
|
||||
|
||||
@Schema(description = "appSecret(仅本地签名,列表脱敏)")
|
||||
private String appSecret;
|
||||
|
||||
@Schema(description = "对方回调根地址")
|
||||
private String callbackBaseUrl;
|
||||
|
||||
@Schema(description = "订单状态回调路径")
|
||||
private String callbackOrderPath;
|
||||
|
||||
@Schema(description = "资源变更回调路径")
|
||||
private String callbackResourcePath;
|
||||
|
||||
@Schema(description = "1启用回调 0关闭")
|
||||
private String callbackEnabled;
|
||||
|
||||
@Schema(description = "按接口 QPS JSON")
|
||||
private String rateLimitJson;
|
||||
|
||||
@Schema(description = "1启用 0停用")
|
||||
private String status;
|
||||
|
||||
@@ -47,4 +68,9 @@ public class SmHotelDownstreamAppEntity extends Model<SmHotelDownstreamAppEntity
|
||||
@TableLogic
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String delFlag;
|
||||
|
||||
/** 列表展示:密钥脱敏(非表字段) */
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "脱敏后的密钥")
|
||||
private String appSecretMasked;
|
||||
}
|
||||
+17
-2
@@ -25,7 +25,7 @@ public class SmHotelOrderEntity extends Model<SmHotelOrderEntity> {
|
||||
|
||||
private Long downstreamAppId;
|
||||
|
||||
@Schema(description = "下游伙伴编码(环启等)")
|
||||
@Schema(description = "订单来源编码(开放应用/环启/慧订等)")
|
||||
private String partnerCode;
|
||||
|
||||
private Long hotelId;
|
||||
@@ -134,11 +134,26 @@ public class SmHotelOrderEntity extends Model<SmHotelOrderEntity> {
|
||||
@Schema(description = "产品名称")
|
||||
private String productName;
|
||||
|
||||
/** 列表展示:下单来源 APP/ADMIN/DOWNSTREAM(非表字段,由 channelNo 推断) */
|
||||
/** 列表展示:下单来源 APP/ADMIN/LOCAL/DOWNSTREAM(非表字段) */
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "下单来源")
|
||||
private String orderSource;
|
||||
|
||||
/** 列表展示:订单来源文案,如 App / 后台 / 环启·huanqi / 慧订·huiding / 应用名(非表字段) */
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "订单来源展示名")
|
||||
private String orderSourceLabel;
|
||||
|
||||
/** 列表展示:下游协议 huanqi/huiding(非表字段,由 partnerCode 解析) */
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "下游协议")
|
||||
private String partnerProtocol;
|
||||
|
||||
/** 列表展示:下游应用名称(非表字段,由 downstreamAppId 解析) */
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "下游应用名称")
|
||||
private String downstreamAppName;
|
||||
|
||||
/** 列表展示:酒店数据来源 local/shangyou/其它渠道(非表字段) */
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "酒店数据来源")
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.bsd.pjcloud.supermarket.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
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.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TenantTable
|
||||
@TableName("canteen_hotel_user_search_pref")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "移动端酒店用户搜索习惯")
|
||||
public class SmHotelUserSearchPrefEntity extends Model<SmHotelUserSearchPrefEntity> {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
private Long tenantId;
|
||||
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "间数")
|
||||
private Integer rooms;
|
||||
|
||||
@Schema(description = "成人数")
|
||||
private Integer adults;
|
||||
|
||||
@Schema(description = "儿童数")
|
||||
private Integer children;
|
||||
|
||||
@Schema(description = "偏好连住晚数")
|
||||
private Integer nights;
|
||||
|
||||
private String city;
|
||||
|
||||
private String keyword;
|
||||
|
||||
private LocalDate lastCheckIn;
|
||||
|
||||
private LocalDate lastCheckOut;
|
||||
|
||||
@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;
|
||||
}
|
||||
+250
-2
@@ -14,6 +14,10 @@ import com.bsd.pjcloud.supermarket.entity.SmHotelDailyEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelOrderEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelProductEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelUserSearchPrefEntity;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelDailyMapper;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelRoomMapper;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelUserSearchPrefMapper;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelMidService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelOrderService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelService;
|
||||
@@ -26,6 +30,9 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
@@ -53,12 +60,23 @@ public class SmHotelAppController {
|
||||
private static final List<String> DEFAULT_FACILITY_OPTIONS = List.of("含早餐", "免费取消", "免费停车", "立即确认",
|
||||
"限时优惠", "接站服务");
|
||||
|
||||
private static final DateTimeFormatter DAY = DateTimeFormatter.ISO_LOCAL_DATE;
|
||||
|
||||
/** 候选酒店 id 用 IN 时的上限;过大则回退 EXISTS,避免超长 SQL */
|
||||
private static final int MAX_IN_HOTEL_IDS = 8000;
|
||||
|
||||
private final SmHotelService hotelService;
|
||||
|
||||
private final SmHotelMidService midService;
|
||||
|
||||
private final SmHotelOrderService orderService;
|
||||
|
||||
private final SmHotelUserSearchPrefMapper userSearchPrefMapper;
|
||||
|
||||
private final SmHotelDailyMapper dailyMapper;
|
||||
|
||||
private final SmHotelRoomMapper roomMapper;
|
||||
|
||||
@Operation(summary = "列表筛选项(区域/星级/设施)")
|
||||
@GetMapping("/filter-meta")
|
||||
public R<Map<String, Object>> filterMeta() {
|
||||
@@ -114,7 +132,7 @@ public class SmHotelAppController {
|
||||
return R.ok(out);
|
||||
}
|
||||
|
||||
@Operation(summary = "上架酒店分页(关键字/区域/星级/评分/价格/设施)")
|
||||
@Operation(summary = "上架酒店分页(关键字/区域/星级/评分/价格/设施/人数/入住区间)")
|
||||
@GetMapping("/page")
|
||||
public R<Page<Map<String, Object>>> page(@ParameterObject Page<SmHotelEntity> page,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@@ -126,7 +144,13 @@ public class SmHotelAppController {
|
||||
@RequestParam(required = false) BigDecimal minScore,
|
||||
@RequestParam(required = false) BigDecimal minPrice,
|
||||
@RequestParam(required = false) BigDecimal maxPrice,
|
||||
@RequestParam(required = false) String facilityTags) {
|
||||
@RequestParam(required = false) String facilityTags,
|
||||
@RequestParam(required = false) Integer rooms,
|
||||
@RequestParam(required = false) Integer adults,
|
||||
@RequestParam(required = false) Integer children,
|
||||
@RequestParam(required = false) Integer guests,
|
||||
@RequestParam(required = false) String checkIn,
|
||||
@RequestParam(required = false) String checkOut) {
|
||||
List<Integer> starList = parseIntCsv(starLevels);
|
||||
List<String> facilityList = parseStrCsv(facilityTags);
|
||||
var wrapper = Wrappers.<SmHotelEntity>lambdaQuery()
|
||||
@@ -159,6 +183,26 @@ public class SmHotelAppController {
|
||||
for (String tag : facilityList) {
|
||||
wrapper.like(SmHotelEntity::getFacilityTags, tag);
|
||||
}
|
||||
LocalDate stayFrom = parseDay(checkIn);
|
||||
LocalDate stayToExclusive = parseDay(checkOut);
|
||||
int guestTotal = resolveGuestTotal(guests, adults, children);
|
||||
int roomCnt = rooms == null || rooms < 1 ? 1 : Math.min(rooms, 20);
|
||||
int needCap = guestTotal > 0 ? Math.max(1, (guestTotal + roomCnt - 1) / roomCnt) : 1;
|
||||
// 先查候选酒店 id(DISTINCT),再 IN;避免对大表相关 EXISTS + ORDER BY 拖慢分页
|
||||
List<Long> candidateIds = resolveAppListCandidateHotelIds(stayFrom, stayToExclusive, needCap);
|
||||
if (candidateIds != null) {
|
||||
if (candidateIds.isEmpty()) {
|
||||
Page<Map<String, Object>> empty = new Page<>(page.getCurrent(), page.getSize(), 0);
|
||||
empty.setRecords(List.of());
|
||||
return R.ok(empty);
|
||||
}
|
||||
wrapper.in(SmHotelEntity::getId, candidateIds);
|
||||
}
|
||||
else {
|
||||
// 候选过多:回退轻量 EXISTS(不再 join product)
|
||||
applyBookableHotelFilter(wrapper, stayFrom, stayToExclusive);
|
||||
applyGuestCapacityFilter(wrapper, needCap);
|
||||
}
|
||||
// 起价区间:按产品最低卖价过滤(分页前约束)
|
||||
if (minPrice != null || maxPrice != null) {
|
||||
List<Long> priceHitIds = loadHotelIdsByMinPriceRange(minPrice, maxPrice);
|
||||
@@ -181,6 +225,70 @@ public class SmHotelAppController {
|
||||
return R.ok(out);
|
||||
}
|
||||
|
||||
@Operation(summary = "读取当前用户酒店搜索习惯")
|
||||
@GetMapping("/search-pref")
|
||||
public R<Map<String, Object>> getSearchPref() {
|
||||
PjcloudUser user = SecurityUtils.getUser();
|
||||
if (user == null || user.getId() == null) {
|
||||
return R.failed("请先登录");
|
||||
}
|
||||
SmHotelUserSearchPrefEntity row = userSearchPrefMapper.selectOne(Wrappers.<SmHotelUserSearchPrefEntity>lambdaQuery()
|
||||
.eq(SmHotelUserSearchPrefEntity::getUserId, user.getId())
|
||||
.last("limit 1"));
|
||||
if (row == null) {
|
||||
return R.ok(defaultSearchPref());
|
||||
}
|
||||
return R.ok(toSearchPrefMap(row));
|
||||
}
|
||||
|
||||
@Operation(summary = "保存当前用户酒店搜索习惯")
|
||||
@PostMapping("/search-pref")
|
||||
public R<Map<String, Object>> saveSearchPref(@RequestBody Map<String, Object> body) {
|
||||
PjcloudUser user = SecurityUtils.getUser();
|
||||
if (user == null || user.getId() == null) {
|
||||
return R.failed("请先登录");
|
||||
}
|
||||
if (body == null) {
|
||||
body = Map.of();
|
||||
}
|
||||
int rooms = clampInt(body.get("rooms"), 1, 10, 1);
|
||||
int adults = clampInt(body.get("adults"), 1, 20, 1);
|
||||
int children = clampInt(body.get("children"), 0, 20, 0);
|
||||
int nights = clampInt(body.get("nights"), 1, 30, 1);
|
||||
LocalDate checkIn = parseDay(str(body.get("checkIn")));
|
||||
LocalDate checkOut = parseDay(str(body.get("checkOut")));
|
||||
if (checkIn != null && checkOut != null && checkOut.isAfter(checkIn)) {
|
||||
nights = (int) Math.min(30, Math.max(1, ChronoUnit.DAYS.between(checkIn, checkOut)));
|
||||
}
|
||||
SmHotelUserSearchPrefEntity row = userSearchPrefMapper.selectOne(Wrappers.<SmHotelUserSearchPrefEntity>lambdaQuery()
|
||||
.eq(SmHotelUserSearchPrefEntity::getUserId, user.getId())
|
||||
.last("limit 1"));
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (row == null) {
|
||||
row = new SmHotelUserSearchPrefEntity();
|
||||
row.setUserId(user.getId());
|
||||
row.setCreateBy(String.valueOf(user.getId()));
|
||||
row.setCreateTime(now);
|
||||
}
|
||||
row.setRooms(rooms);
|
||||
row.setAdults(adults);
|
||||
row.setChildren(children);
|
||||
row.setNights(nights);
|
||||
row.setCity(StrUtil.maxLength(StrUtil.trim(str(body.get("city"))), 64));
|
||||
row.setKeyword(StrUtil.maxLength(StrUtil.trim(str(body.get("keyword"))), 128));
|
||||
row.setLastCheckIn(checkIn);
|
||||
row.setLastCheckOut(checkOut);
|
||||
row.setUpdateBy(String.valueOf(user.getId()));
|
||||
row.setUpdateTime(now);
|
||||
if (row.getId() == null) {
|
||||
userSearchPrefMapper.insert(row);
|
||||
}
|
||||
else {
|
||||
userSearchPrefMapper.updateById(row);
|
||||
}
|
||||
return R.ok(toSearchPrefMap(row));
|
||||
}
|
||||
|
||||
@Operation(summary = "酒店详情(含房型/产品)")
|
||||
@GetMapping("/detail/{id}")
|
||||
public R<SmHotelDetailVO> detail(@PathVariable Long id) {
|
||||
@@ -506,6 +614,146 @@ public class SmHotelAppController {
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析列表候选酒店:
|
||||
* 1) 入住区间内有日历;
|
||||
* 2) 可选:房型可住人数达标。
|
||||
* 返回 null 表示候选过多,调用方回退 EXISTS。
|
||||
*/
|
||||
private List<Long> resolveAppListCandidateHotelIds(LocalDate stayFrom, LocalDate stayToExclusive, int needCapPerRoom) {
|
||||
LocalDate from = stayFrom != null ? stayFrom : LocalDate.now();
|
||||
LocalDate toEx = stayToExclusive;
|
||||
if (toEx == null || !toEx.isAfter(from)) {
|
||||
// 未传离店:只扫未来 30 天有日历的酒店,避免无上界扫全表
|
||||
toEx = from.plusDays(30);
|
||||
}
|
||||
List<Long> bookable = dailyMapper.selectDistinctHotelIdsByStayRange(from, toEx);
|
||||
if (bookable == null || bookable.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<Long> ids = bookable.stream().filter(Objects::nonNull).distinct().toList();
|
||||
if (needCapPerRoom > 1) {
|
||||
List<Long> capHotels = roomMapper.selectDistinctHotelIdsByMinCapacity(needCapPerRoom);
|
||||
if (capHotels == null || capHotels.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
Set<Long> capSet = new java.util.HashSet<>(capHotels);
|
||||
ids = ids.stream().filter(capSet::contains).toList();
|
||||
if (ids.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
if (ids.size() > MAX_IN_HOTEL_IDS) {
|
||||
return null;
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
/** 轻量 EXISTS:仅日历表,不再 join product */
|
||||
private void applyBookableHotelFilter(LambdaQueryWrapper<SmHotelEntity> wrapper, LocalDate stayFrom,
|
||||
LocalDate stayToExclusive) {
|
||||
LocalDate from = stayFrom != null ? stayFrom : LocalDate.now();
|
||||
LocalDate toEx = stayToExclusive;
|
||||
if (toEx == null || !toEx.isAfter(from)) {
|
||||
toEx = from.plusDays(30);
|
||||
}
|
||||
wrapper.apply("""
|
||||
EXISTS (
|
||||
SELECT 1 FROM canteen_hotel_daily d
|
||||
WHERE d.hotel_id = canteen_hotel.id
|
||||
AND d.del_flag = '0'
|
||||
AND d.stay_date >= {0}
|
||||
AND d.stay_date < {1}
|
||||
)
|
||||
""", from, toEx);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按房型可住人数筛选:至少有一个房型 capacity >= 每间所需人数;
|
||||
* capacity 为空的房型视为未知容量,仍保留(避免老数据全被滤掉)。
|
||||
*/
|
||||
private void applyGuestCapacityFilter(LambdaQueryWrapper<SmHotelEntity> wrapper, int needCapPerRoom) {
|
||||
if (needCapPerRoom <= 1) {
|
||||
return;
|
||||
}
|
||||
wrapper.apply("""
|
||||
EXISTS (
|
||||
SELECT 1 FROM canteen_hotel_room r
|
||||
WHERE r.hotel_id = canteen_hotel.id
|
||||
AND r.del_flag = '0'
|
||||
AND (r.capacity IS NULL OR r.capacity >= {0})
|
||||
)
|
||||
""", needCapPerRoom);
|
||||
}
|
||||
|
||||
private static int resolveGuestTotal(Integer guests, Integer adults, Integer children) {
|
||||
if (guests != null && guests > 0) {
|
||||
return Math.min(guests, 40);
|
||||
}
|
||||
int a = adults == null ? 0 : Math.max(0, adults);
|
||||
int c = children == null ? 0 : Math.max(0, children);
|
||||
int total = a + c;
|
||||
return Math.min(total, 40);
|
||||
}
|
||||
|
||||
private static LocalDate parseDay(String raw) {
|
||||
if (StrUtil.isBlank(raw)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String s = raw.trim().replace('/', '-');
|
||||
if (s.length() >= 10) {
|
||||
s = s.substring(0, 10);
|
||||
}
|
||||
return LocalDate.parse(s, DAY);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static int clampInt(Object raw, int min, int max, int def) {
|
||||
if (raw == null) {
|
||||
return def;
|
||||
}
|
||||
try {
|
||||
int v = Integer.parseInt(String.valueOf(raw).trim());
|
||||
return Math.max(min, Math.min(max, v));
|
||||
}
|
||||
catch (Exception e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> defaultSearchPref() {
|
||||
Map<String, Object> m = new LinkedHashMap<>();
|
||||
m.put("rooms", 1);
|
||||
m.put("adults", 1);
|
||||
m.put("children", 0);
|
||||
m.put("nights", 1);
|
||||
m.put("city", "");
|
||||
m.put("keyword", "");
|
||||
m.put("checkIn", null);
|
||||
m.put("checkOut", null);
|
||||
return m;
|
||||
}
|
||||
|
||||
private static Map<String, Object> toSearchPrefMap(SmHotelUserSearchPrefEntity row) {
|
||||
Map<String, Object> m = defaultSearchPref();
|
||||
if (row == null) {
|
||||
return m;
|
||||
}
|
||||
m.put("rooms", row.getRooms() == null ? 1 : row.getRooms());
|
||||
m.put("adults", row.getAdults() == null ? 1 : row.getAdults());
|
||||
m.put("children", row.getChildren() == null ? 0 : row.getChildren());
|
||||
m.put("nights", row.getNights() == null ? 1 : row.getNights());
|
||||
m.put("city", StrUtil.blankToDefault(row.getCity(), ""));
|
||||
m.put("keyword", StrUtil.blankToDefault(row.getKeyword(), ""));
|
||||
m.put("checkIn", row.getLastCheckIn() == null ? null : row.getLastCheckIn().toString());
|
||||
m.put("checkOut", row.getLastCheckOut() == null ? null : row.getLastCheckOut().toString());
|
||||
return m;
|
||||
}
|
||||
|
||||
/** 按产品最低卖价落在区间内的酒店 id(起价筛选) */
|
||||
private List<Long> loadHotelIdsByMinPriceRange(BigDecimal minPrice, BigDecimal maxPrice) {
|
||||
List<SmHotelProductEntity> products = Db.lambdaQuery(SmHotelProductEntity.class)
|
||||
|
||||
+2
@@ -39,6 +39,8 @@ public class SmHotelController {
|
||||
.like(StrUtil.isNotBlank(query.getGroupCode()), SmHotelEntity::getGroupCode, query.getGroupCode())
|
||||
.eq(StrUtil.isNotBlank(query.getSourceId()), SmHotelEntity::getSourceId, query.getSourceId())
|
||||
.eq(StrUtil.isNotBlank(query.getSheetName()), SmHotelEntity::getSheetName, query.getSheetName())
|
||||
// 归属部门:渠道映射按部门推送时,便于按部门筛酒店核查
|
||||
.eq(query.getDeptId() != null, SmHotelEntity::getDeptId, query.getDeptId())
|
||||
.orderByDesc(SmHotelEntity::getCreateTime)
|
||||
.orderByDesc(SmHotelEntity::getId);
|
||||
// 位置区域:省/市前缀覆盖下级,并兼容仅有区域名称的老数据
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
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.SmHotelDownstreamAppEntity;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelDownstreamAppService;
|
||||
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("/smHotelDownstreamApp")
|
||||
@Tag(name = "酒店下游开放应用")
|
||||
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
|
||||
public class SmHotelDownstreamAppController {
|
||||
|
||||
private final SmHotelDownstreamAppService appService;
|
||||
|
||||
@Operation(summary = "开放应用分页")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelDownstreamApp_view')")
|
||||
public R<Page<SmHotelDownstreamAppEntity>> page(@ParameterObject Page<SmHotelDownstreamAppEntity> page,
|
||||
@ParameterObject SmHotelDownstreamAppEntity query) {
|
||||
return R.ok(appService.pageApps(page, query));
|
||||
}
|
||||
|
||||
@Operation(summary = "开放应用详情")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelDownstreamApp_view')")
|
||||
public R<SmHotelDownstreamAppEntity> detail(@PathVariable Long id) {
|
||||
SmHotelDownstreamAppEntity app = appService.getDetail(id);
|
||||
return app == null ? R.failed("应用不存在") : R.ok(app);
|
||||
}
|
||||
|
||||
@SysLog("新增下游开放应用")
|
||||
@Operation(summary = "新建开放应用(返回明文 secret 仅此一次)")
|
||||
@PostMapping
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelDownstreamApp_edit')")
|
||||
public R<SmHotelDownstreamAppEntity> create(@RequestBody SmHotelDownstreamAppEntity body) {
|
||||
return appService.createApp(body);
|
||||
}
|
||||
|
||||
@SysLog("修改下游开放应用")
|
||||
@Operation(summary = "更新开放应用(不含密钥)")
|
||||
@PutMapping
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelDownstreamApp_edit')")
|
||||
public R<SmHotelDownstreamAppEntity> update(@RequestBody SmHotelDownstreamAppEntity body) {
|
||||
return appService.updateApp(body);
|
||||
}
|
||||
|
||||
@SysLog("删除下游开放应用")
|
||||
@Operation(summary = "删除开放应用")
|
||||
@DeleteMapping
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelDownstreamApp_edit')")
|
||||
public R<Boolean> remove(@RequestBody Long[] ids) {
|
||||
return appService.removeApps(ids);
|
||||
}
|
||||
|
||||
@SysLog("轮换下游开放应用密钥")
|
||||
@Operation(summary = "重新生成 secret(返回明文仅此一次)")
|
||||
@PostMapping("/rotateSecret/{id}")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelDownstreamApp_edit')")
|
||||
public R<SmHotelDownstreamAppEntity> rotateSecret(@PathVariable Long id) {
|
||||
return appService.rotateSecret(id);
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package com.bsd.pjcloud.supermarket.controller;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.security.annotation.Inner;
|
||||
import com.bsd.pjcloud.supermarket.constant.SmHotelOpenApi;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelDownstreamAppEntity;
|
||||
import com.bsd.pjcloud.supermarket.integration.downstream.DownstreamInboundAuth;
|
||||
import com.bsd.pjcloud.supermarket.integration.downstream.DownstreamOpenRateLimiter;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelOpenApiService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 酒店下游统一开放 API(对方按我方规范对接)。
|
||||
* 鉴权头:appKey / timestamp / nonce / sign;签名同环启 MD5 规则。
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping(SmHotelOpenApi.BASE)
|
||||
@Tag(name = "酒店下游开放平台")
|
||||
public class SmHotelOpenApiController {
|
||||
|
||||
private final DownstreamInboundAuth inboundAuth;
|
||||
|
||||
private final DownstreamOpenRateLimiter rateLimiter;
|
||||
|
||||
private final SmHotelOpenApiService openApiService;
|
||||
|
||||
@Inner(value = false)
|
||||
@Operation(summary = "酒店列表")
|
||||
@PostMapping("/hotel/list")
|
||||
public R<Map<String, Object>> hotelList(HttpServletRequest request, @RequestBody String rawBody) {
|
||||
return guarded(request, rawBody, SmHotelOpenApi.API_HOTEL_LIST,
|
||||
(app, body) -> openApiService.hotelList(app, body));
|
||||
}
|
||||
|
||||
@Inner(value = false)
|
||||
@Operation(summary = "酒店详情(含房型/产品)")
|
||||
@PostMapping("/hotel/detail")
|
||||
public R<Map<String, Object>> hotelDetail(HttpServletRequest request, @RequestBody String rawBody) {
|
||||
return guarded(request, rawBody, SmHotelOpenApi.API_HOTEL_DETAIL,
|
||||
(app, body) -> openApiService.hotelDetail(app, body));
|
||||
}
|
||||
|
||||
@Inner(value = false)
|
||||
@Operation(summary = "价态/库存日历")
|
||||
@PostMapping("/hotel/daily")
|
||||
public R<Map<String, Object>> hotelDaily(HttpServletRequest request, @RequestBody String rawBody) {
|
||||
return guarded(request, rawBody, SmHotelOpenApi.API_HOTEL_DAILY,
|
||||
(app, body) -> openApiService.hotelDaily(app, body));
|
||||
}
|
||||
|
||||
@Inner(value = false)
|
||||
@Operation(summary = "下单前可订校验")
|
||||
@PostMapping("/order/check")
|
||||
public R<Map<String, Object>> orderCheck(HttpServletRequest request, @RequestBody String rawBody) {
|
||||
return guarded(request, rawBody, SmHotelOpenApi.API_ORDER_CHECK,
|
||||
(app, body) -> openApiService.orderCheck(app, body));
|
||||
}
|
||||
|
||||
@Inner(value = false)
|
||||
@Operation(summary = "创建订单")
|
||||
@PostMapping("/order/create")
|
||||
public R<JSONObject> orderCreate(HttpServletRequest request, @RequestBody String rawBody) {
|
||||
return guarded(request, rawBody, SmHotelOpenApi.API_ORDER_CREATE,
|
||||
(app, body) -> openApiService.orderCreate(app, body));
|
||||
}
|
||||
|
||||
@Inner(value = false)
|
||||
@Operation(summary = "取消订单")
|
||||
@PostMapping("/order/cancel")
|
||||
public R<JSONObject> orderCancel(HttpServletRequest request, @RequestBody String rawBody) {
|
||||
return guarded(request, rawBody, SmHotelOpenApi.API_ORDER_CANCEL,
|
||||
(app, body) -> openApiService.orderCancel(app, body));
|
||||
}
|
||||
|
||||
@Inner(value = false)
|
||||
@Operation(summary = "查询订单")
|
||||
@PostMapping("/order/query")
|
||||
public R<JSONObject> orderQuery(HttpServletRequest request, @RequestBody String rawBody) {
|
||||
return guarded(request, rawBody, SmHotelOpenApi.API_ORDER_QUERY,
|
||||
(app, body) -> openApiService.orderQuery(app, body));
|
||||
}
|
||||
|
||||
private <T> R<T> guarded(HttpServletRequest request, String rawBody, String apiCode, Handler<T> handler) {
|
||||
try {
|
||||
String appKey = header(request, "appKey");
|
||||
SmHotelDownstreamAppEntity app = inboundAuth.verify(appKey, header(request, "timestamp"),
|
||||
header(request, "nonce"), header(request, "sign"), rawBody);
|
||||
rateLimiter.acquire(app, apiCode);
|
||||
Map<String, Object> body = JSONUtil.parseObj(StrUtil.blankToDefault(rawBody, "{}"));
|
||||
return handler.handle(app, body);
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|开放API|{}|失败|{}", apiCode, e.getMessage());
|
||||
return R.failed(StrUtil.blankToDefault(e.getMessage(), "失败"));
|
||||
}
|
||||
}
|
||||
|
||||
private static String header(HttpServletRequest request, String name) {
|
||||
String v = request.getHeader(name);
|
||||
return v != null ? v : request.getParameter(name);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface Handler<T> {
|
||||
|
||||
R<T> handle(SmHotelDownstreamAppEntity app, Map<String, Object> body);
|
||||
}
|
||||
}
|
||||
+7
@@ -34,6 +34,13 @@ public class SmHotelOrderController {
|
||||
return R.ok(orderService.pageOrders(page, query));
|
||||
}
|
||||
|
||||
@Operation(summary = "订单来源筛选项(渠道下游 + 开放应用)")
|
||||
@GetMapping("/orderSourceOptions")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelOrder_view')")
|
||||
public R<java.util.List<java.util.Map<String, Object>>> orderSourceOptions() {
|
||||
return R.ok(orderService.listOrderSourceOptions());
|
||||
}
|
||||
|
||||
@Operation(summary = "订单详情(含酒店/产品/日历)")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelOrder_view')")
|
||||
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
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.JSONUtil;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelDownstreamAppEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelOrderEntity;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelDownstreamAppMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* 开放平台 → 下游回调(订单状态等)。失败仅打日志,不阻断主流程。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DownstreamOpenCallbackClient {
|
||||
|
||||
private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(4, r -> {
|
||||
Thread t = new Thread(r, "sm-hotel-open-callback");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
private final SmHotelDownstreamAppMapper appMapper;
|
||||
|
||||
public void notifyOrderStatusAsync(Long downstreamAppId, SmHotelOrderEntity order) {
|
||||
if (downstreamAppId == null || order == null) {
|
||||
return;
|
||||
}
|
||||
EXECUTOR.execute(() -> {
|
||||
try {
|
||||
notifyOrderStatus(downstreamAppId, order);
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|开放回调|订单状态失败|appId={}|channelNo={}|{}", downstreamAppId, order.getChannelNo(),
|
||||
e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void notifyOrderStatus(Long downstreamAppId, SmHotelOrderEntity order) {
|
||||
SmHotelDownstreamAppEntity app = appMapper.selectById(downstreamAppId);
|
||||
if (app == null || !"1".equals(app.getStatus()) || !"1".equals(app.getCallbackEnabled())) {
|
||||
return;
|
||||
}
|
||||
if (StrUtil.isBlank(app.getCallbackBaseUrl())) {
|
||||
return;
|
||||
}
|
||||
String path = StrUtil.blankToDefault(StrUtil.trim(app.getCallbackOrderPath()), "/order/status/notify");
|
||||
String url = joinUrl(app.getCallbackBaseUrl(), path);
|
||||
Map<String, Object> body = new LinkedHashMap<>(12);
|
||||
body.put("channelNo", order.getChannelNo());
|
||||
body.put("orderNo", order.getId() == null ? null : String.valueOf(order.getId()));
|
||||
body.put("platformNo", order.getPlatformNo());
|
||||
body.put("orderStatus", order.getOrderStatus());
|
||||
body.put("bookStatus", order.getBookStatus());
|
||||
body.put("hotelCode", order.getHotelCode());
|
||||
body.put("productCode", order.getProductCode());
|
||||
body.put("sellAmount", order.getSellAmount());
|
||||
body.put("failMsg", order.getFailMsg());
|
||||
String bodyJson = JSONUtil.toJsonStr(body);
|
||||
String timestamp = String.valueOf(System.currentTimeMillis());
|
||||
String nonce = IdUtil.fastSimpleUUID();
|
||||
String sign = DownstreamSignHelper.sign(app.getAppKey(), app.getAppSecret(), timestamp, nonce, bodyJson);
|
||||
try (HttpResponse res = HttpRequest.post(url)
|
||||
.header("appKey", app.getAppKey())
|
||||
.header("timestamp", timestamp)
|
||||
.header("nonce", nonce)
|
||||
.header("sign", sign)
|
||||
.header("Content-Type", "application/json;charset=UTF-8")
|
||||
.body(bodyJson)
|
||||
.timeout(15000)
|
||||
.execute()) {
|
||||
log.info("|开放回调|订单状态|appKey={}|url={}|http={}|channelNo={}|", app.getAppKey(), url, res.getStatus(),
|
||||
order.getChannelNo());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源变更通知(房价/库存等)。路径未配置则跳过。
|
||||
* 后续由本地价态变更事件批量触发;当前可供手动/任务调用。
|
||||
*/
|
||||
public void notifyResourceChangeAsync(Long downstreamAppId, String hotelCode, String changeType,
|
||||
Object payload) {
|
||||
if (downstreamAppId == null || StrUtil.isBlank(hotelCode)) {
|
||||
return;
|
||||
}
|
||||
EXECUTOR.execute(() -> {
|
||||
try {
|
||||
SmHotelDownstreamAppEntity app = appMapper.selectById(downstreamAppId);
|
||||
if (app == null || !"1".equals(app.getStatus()) || !"1".equals(app.getCallbackEnabled())) {
|
||||
return;
|
||||
}
|
||||
if (StrUtil.isBlank(app.getCallbackBaseUrl()) || StrUtil.isBlank(app.getCallbackResourcePath())) {
|
||||
return;
|
||||
}
|
||||
String url = joinUrl(app.getCallbackBaseUrl(), app.getCallbackResourcePath());
|
||||
Map<String, Object> body = new LinkedHashMap<>(8);
|
||||
body.put("hotelCode", hotelCode);
|
||||
body.put("changeType", StrUtil.blankToDefault(changeType, "DAILY"));
|
||||
body.put("payload", payload);
|
||||
String bodyJson = JSONUtil.toJsonStr(body);
|
||||
String timestamp = String.valueOf(System.currentTimeMillis());
|
||||
String nonce = IdUtil.fastSimpleUUID();
|
||||
String sign = DownstreamSignHelper.sign(app.getAppKey(), app.getAppSecret(), timestamp, nonce,
|
||||
bodyJson);
|
||||
try (HttpResponse res = HttpRequest.post(url)
|
||||
.header("appKey", app.getAppKey())
|
||||
.header("timestamp", timestamp)
|
||||
.header("nonce", nonce)
|
||||
.header("sign", sign)
|
||||
.header("Content-Type", "application/json;charset=UTF-8")
|
||||
.body(bodyJson)
|
||||
.timeout(15000)
|
||||
.execute()) {
|
||||
log.info("|开放回调|资源变更|appKey={}|hotel={}|http={}|", app.getAppKey(), hotelCode,
|
||||
res.getStatus());
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|开放回调|资源变更失败|appId={}|hotel={}|{}", downstreamAppId, hotelCode, e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static String joinUrl(String base, String path) {
|
||||
String b = StrUtil.removeSuffix(StrUtil.trim(base), "/");
|
||||
String p = StrUtil.trim(path);
|
||||
if (!p.startsWith("/")) {
|
||||
p = "/" + p;
|
||||
}
|
||||
return b + p;
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package com.bsd.pjcloud.supermarket.integration.downstream;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.bsd.pjcloud.common.core.exception.CheckedException;
|
||||
import com.bsd.pjcloud.supermarket.constant.SmHotelOpenApi;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelDownstreamAppEntity;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 开放平台入站限流:按 appKey + 接口码独立秒窗 QPS,应用间互不影响。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DownstreamOpenRateLimiter {
|
||||
|
||||
private static final String KEY = "sm:hotel:open:qps:";
|
||||
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
|
||||
public void acquire(SmHotelDownstreamAppEntity app, String apiCode) {
|
||||
if (app == null || StrUtil.isBlank(app.getAppKey())) {
|
||||
return;
|
||||
}
|
||||
int qps = resolveQps(app.getRateLimitJson(), apiCode);
|
||||
if (qps <= 0) {
|
||||
return;
|
||||
}
|
||||
long second = System.currentTimeMillis() / 1000L;
|
||||
String redisKey = KEY + app.getAppKey() + ":" + StrUtil.blankToDefault(apiCode, "default") + ":" + second;
|
||||
Long n = redisTemplate.opsForValue().increment(redisKey);
|
||||
if (n != null && n == 1L) {
|
||||
redisTemplate.expire(redisKey, 3, TimeUnit.SECONDS);
|
||||
}
|
||||
if (n != null && n > qps) {
|
||||
log.warn("|开放平台限流|appKey={}|api={}|qps={}|hit={}|", app.getAppKey(), apiCode, qps, n);
|
||||
throw new CheckedException("请求过于频繁,请降低调用频率(" + apiCode + " QPS=" + qps + ")");
|
||||
}
|
||||
}
|
||||
|
||||
public static int resolveQps(String rateLimitJson, String apiCode) {
|
||||
int def = SmHotelOpenApi.DEFAULT_QPS;
|
||||
if (StrUtil.isBlank(rateLimitJson)) {
|
||||
return defaultForApi(apiCode, def);
|
||||
}
|
||||
try {
|
||||
JSONObject jo = JSONUtil.parseObj(rateLimitJson);
|
||||
if (StrUtil.isNotBlank(apiCode) && jo.get(apiCode) != null) {
|
||||
return Math.max(0, jo.getInt(apiCode));
|
||||
}
|
||||
if (jo.get("default") != null) {
|
||||
def = Math.max(0, jo.getInt("default"));
|
||||
}
|
||||
return defaultForApi(apiCode, def);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return defaultForApi(apiCode, def);
|
||||
}
|
||||
}
|
||||
|
||||
private static int defaultForApi(String apiCode, int def) {
|
||||
if (SmHotelOpenApi.API_ORDER_CREATE.equals(apiCode) || SmHotelOpenApi.API_ORDER_CANCEL.equals(apiCode)) {
|
||||
return Math.min(def, 5);
|
||||
}
|
||||
if (SmHotelOpenApi.API_HOTEL_LIST.equals(apiCode) || SmHotelOpenApi.API_ORDER_CHECK.equals(apiCode)) {
|
||||
return Math.min(def, 10);
|
||||
}
|
||||
return def;
|
||||
}
|
||||
}
|
||||
+12
@@ -30,6 +30,18 @@ public interface SmHotelDailyMapper extends PjcloudBaseMapper<SmHotelDailyEntity
|
||||
int physicalDeleteRange(@Param("hotelId") Long hotelId, @Param("fromDate") LocalDate fromDate,
|
||||
@Param("toExclusive") LocalDate toExclusive);
|
||||
|
||||
/**
|
||||
* 移动端列表:入住半开区间内有日历的酒店(DISTINCT,避免相关子查询扫大表)。
|
||||
*/
|
||||
@Select("""
|
||||
SELECT DISTINCT hotel_id FROM canteen_hotel_daily
|
||||
WHERE del_flag = '0'
|
||||
AND stay_date >= #{fromDate}
|
||||
AND stay_date < #{toExclusive}
|
||||
""")
|
||||
List<Long> selectDistinctHotelIdsByStayRange(@Param("fromDate") LocalDate fromDate,
|
||||
@Param("toExclusive") LocalDate toExclusive);
|
||||
|
||||
int insertBatch(@Param("list") List<SmHotelDailyEntity> list);
|
||||
|
||||
/**
|
||||
|
||||
+8
@@ -24,4 +24,12 @@ public interface SmHotelRoomMapper extends PjcloudBaseMapper<SmHotelRoomEntity>
|
||||
</script>
|
||||
""")
|
||||
List<Map<String, Object>> countGroupByHotelIds(@Param("ids") Collection<Long> ids);
|
||||
|
||||
/** 移动端列表:房型可住人数达标(或未填容量)的酒店 */
|
||||
@Select("""
|
||||
SELECT DISTINCT hotel_id FROM canteen_hotel_room
|
||||
WHERE del_flag = '0'
|
||||
AND (capacity IS NULL OR capacity >= #{minCapacity})
|
||||
""")
|
||||
List<Long> selectDistinctHotelIdsByMinCapacity(@Param("minCapacity") int minCapacity);
|
||||
}
|
||||
+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.SmHotelUserSearchPrefEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface SmHotelUserSearchPrefMapper extends PjcloudBaseMapper<SmHotelUserSearchPrefEntity> {
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
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.SmHotelDownstreamAppEntity;
|
||||
|
||||
public interface SmHotelDownstreamAppService extends IService<SmHotelDownstreamAppEntity> {
|
||||
|
||||
Page<SmHotelDownstreamAppEntity> pageApps(Page<SmHotelDownstreamAppEntity> page, SmHotelDownstreamAppEntity query);
|
||||
|
||||
SmHotelDownstreamAppEntity getDetail(Long id);
|
||||
|
||||
R<SmHotelDownstreamAppEntity> createApp(SmHotelDownstreamAppEntity entity);
|
||||
|
||||
R<SmHotelDownstreamAppEntity> updateApp(SmHotelDownstreamAppEntity entity);
|
||||
|
||||
R<Boolean> removeApps(Long[] ids);
|
||||
|
||||
/** 重新生成密钥,返回含明文 secret 的对象(仅此一次) */
|
||||
R<SmHotelDownstreamAppEntity> rotateSecret(Long id);
|
||||
|
||||
SmHotelDownstreamAppEntity getEnabledByAppKey(String appKey);
|
||||
|
||||
/** 订单来源编码:优先 partnerCode,否则 appKey */
|
||||
String resolvePartnerCode(SmHotelDownstreamAppEntity app);
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.bsd.pjcloud.supermarket.service;
|
||||
|
||||
import cn.hutool.json.JSONObject;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelDownstreamAppEntity;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 下游统一开放 API 业务(对方调我方)。
|
||||
*/
|
||||
public interface SmHotelOpenApiService {
|
||||
|
||||
R<Map<String, Object>> hotelList(SmHotelDownstreamAppEntity app, Map<String, Object> body);
|
||||
|
||||
R<Map<String, Object>> hotelDetail(SmHotelDownstreamAppEntity app, Map<String, Object> body);
|
||||
|
||||
R<Map<String, Object>> hotelDaily(SmHotelDownstreamAppEntity app, Map<String, Object> body);
|
||||
|
||||
R<Map<String, Object>> orderCheck(SmHotelDownstreamAppEntity app, Map<String, Object> body);
|
||||
|
||||
R<JSONObject> orderCreate(SmHotelDownstreamAppEntity app, Map<String, Object> body);
|
||||
|
||||
R<JSONObject> orderCancel(SmHotelDownstreamAppEntity app, Map<String, Object> body);
|
||||
|
||||
R<JSONObject> orderQuery(SmHotelDownstreamAppEntity app, Map<String, Object> body);
|
||||
}
|
||||
+3
@@ -17,6 +17,9 @@ public interface SmHotelOrderService {
|
||||
|
||||
Page<SmHotelOrderEntity> pageOrders(Page<SmHotelOrderEntity> page, SmHotelOrderEntity query);
|
||||
|
||||
/** 订单来源筛选项:渠道下游 + 开放应用(各自显示名称) */
|
||||
List<Map<String, Object>> listOrderSourceOptions();
|
||||
|
||||
/** App「我的订单」:按用户 ID 绑定(channelNo 前缀 APP{userId}) */
|
||||
Page<SmHotelOrderEntity> pageMyAppOrders(Page<SmHotelOrderEntity> page, PjcloudUser user, String orderStatus);
|
||||
|
||||
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
package com.bsd.pjcloud.supermarket.service.impl;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
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.entity.SmHotelDownstreamAppEntity;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelDownstreamAppMapper;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelDownstreamAppService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class SmHotelDownstreamAppServiceImpl extends ServiceImpl<SmHotelDownstreamAppMapper, SmHotelDownstreamAppEntity>
|
||||
implements SmHotelDownstreamAppService {
|
||||
|
||||
private static final String DEFAULT_RATE_JSON = "{\"default\":20,\"hotel.list\":10,\"hotel.detail\":20,"
|
||||
+ "\"hotel.daily\":20,\"order.check\":10,\"order.create\":5,\"order.cancel\":5,\"order.query\":20}";
|
||||
|
||||
@Override
|
||||
public Page<SmHotelDownstreamAppEntity> pageApps(Page<SmHotelDownstreamAppEntity> page,
|
||||
SmHotelDownstreamAppEntity query) {
|
||||
Page<SmHotelDownstreamAppEntity> result = baseMapper.selectPage(page,
|
||||
Wrappers.<SmHotelDownstreamAppEntity>lambdaQuery()
|
||||
.like(StrUtil.isNotBlank(query.getAppName()), SmHotelDownstreamAppEntity::getAppName,
|
||||
query.getAppName())
|
||||
.like(StrUtil.isNotBlank(query.getAppKey()), SmHotelDownstreamAppEntity::getAppKey,
|
||||
query.getAppKey())
|
||||
.like(StrUtil.isNotBlank(query.getPartnerCode()), SmHotelDownstreamAppEntity::getPartnerCode,
|
||||
query.getPartnerCode())
|
||||
.eq(StrUtil.isNotBlank(query.getStatus()), SmHotelDownstreamAppEntity::getStatus,
|
||||
query.getStatus())
|
||||
.orderByDesc(SmHotelDownstreamAppEntity::getUpdateTime)
|
||||
.orderByDesc(SmHotelDownstreamAppEntity::getCreateTime));
|
||||
if (result.getRecords() != null) {
|
||||
result.getRecords().forEach(this::maskSecret);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SmHotelDownstreamAppEntity getDetail(Long id) {
|
||||
SmHotelDownstreamAppEntity app = baseMapper.selectById(id);
|
||||
if (app != null) {
|
||||
maskSecret(app);
|
||||
}
|
||||
return app;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<SmHotelDownstreamAppEntity> createApp(SmHotelDownstreamAppEntity entity) {
|
||||
if (entity == null || StrUtil.isBlank(entity.getAppName())) {
|
||||
return R.failed("应用名称不能为空");
|
||||
}
|
||||
String appKey = StrUtil.trim(entity.getAppKey());
|
||||
if (StrUtil.isBlank(appKey)) {
|
||||
appKey = "OA" + IdWorker.getIdStr();
|
||||
}
|
||||
if (existsAppKey(appKey, null)) {
|
||||
return R.failed("appKey 已存在:" + appKey);
|
||||
}
|
||||
entity.setAppKey(appKey);
|
||||
if (StrUtil.isBlank(entity.getAppSecret())) {
|
||||
entity.setAppSecret(genSecret());
|
||||
}
|
||||
entity.setPartnerCode(StrUtil.blankToDefault(StrUtil.trim(entity.getPartnerCode()), appKey));
|
||||
entity.setStatus(StrUtil.blankToDefault(entity.getStatus(), "1"));
|
||||
entity.setCallbackEnabled(StrUtil.blankToDefault(entity.getCallbackEnabled(), "0"));
|
||||
entity.setCallbackOrderPath(StrUtil.blankToDefault(StrUtil.trim(entity.getCallbackOrderPath()),
|
||||
"/order/status/notify"));
|
||||
if (StrUtil.isBlank(entity.getRateLimitJson())) {
|
||||
entity.setRateLimitJson(DEFAULT_RATE_JSON);
|
||||
}
|
||||
baseMapper.insert(entity);
|
||||
// 创建时回传明文 secret,方便复制发给对方
|
||||
return R.ok(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<SmHotelDownstreamAppEntity> updateApp(SmHotelDownstreamAppEntity entity) {
|
||||
if (entity == null || entity.getId() == null) {
|
||||
return R.failed("id 不能为空");
|
||||
}
|
||||
SmHotelDownstreamAppEntity db = baseMapper.selectById(entity.getId());
|
||||
if (db == null) {
|
||||
return R.failed("应用不存在");
|
||||
}
|
||||
// appKey / secret 不允许在普通更新里改;密钥走 rotate
|
||||
db.setAppName(StrUtil.trim(entity.getAppName()));
|
||||
db.setPartnerCode(StrUtil.blankToDefault(StrUtil.trim(entity.getPartnerCode()), db.getAppKey()));
|
||||
db.setCallbackBaseUrl(StrUtil.trim(entity.getCallbackBaseUrl()));
|
||||
db.setCallbackOrderPath(StrUtil.blankToDefault(StrUtil.trim(entity.getCallbackOrderPath()),
|
||||
"/order/status/notify"));
|
||||
db.setCallbackResourcePath(StrUtil.trim(entity.getCallbackResourcePath()));
|
||||
db.setCallbackEnabled(StrUtil.blankToDefault(entity.getCallbackEnabled(), "0"));
|
||||
if (StrUtil.isNotBlank(entity.getRateLimitJson())) {
|
||||
db.setRateLimitJson(entity.getRateLimitJson());
|
||||
}
|
||||
db.setStatus(StrUtil.blankToDefault(entity.getStatus(), db.getStatus()));
|
||||
db.setRemark(entity.getRemark());
|
||||
baseMapper.updateById(db);
|
||||
maskSecret(db);
|
||||
return R.ok(db);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<Boolean> removeApps(Long[] ids) {
|
||||
if (ids == null || ids.length == 0) {
|
||||
return R.failed("请选择要删除的应用");
|
||||
}
|
||||
List<Long> list = Arrays.asList(ids);
|
||||
baseMapper.deleteByIds(list);
|
||||
return R.ok(Boolean.TRUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<SmHotelDownstreamAppEntity> rotateSecret(Long id) {
|
||||
SmHotelDownstreamAppEntity db = baseMapper.selectById(id);
|
||||
if (db == null) {
|
||||
return R.failed("应用不存在");
|
||||
}
|
||||
db.setAppSecret(genSecret());
|
||||
baseMapper.updateById(db);
|
||||
return R.ok(db);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SmHotelDownstreamAppEntity getEnabledByAppKey(String appKey) {
|
||||
if (StrUtil.isBlank(appKey)) {
|
||||
return null;
|
||||
}
|
||||
return baseMapper.selectOne(Wrappers.<SmHotelDownstreamAppEntity>lambdaQuery()
|
||||
.eq(SmHotelDownstreamAppEntity::getAppKey, appKey.trim())
|
||||
.eq(SmHotelDownstreamAppEntity::getStatus, "1")
|
||||
.last("limit 1"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolvePartnerCode(SmHotelDownstreamAppEntity app) {
|
||||
if (app == null) {
|
||||
return null;
|
||||
}
|
||||
return StrUtil.blankToDefault(StrUtil.trim(app.getPartnerCode()), StrUtil.trim(app.getAppKey()));
|
||||
}
|
||||
|
||||
private boolean existsAppKey(String appKey, Long excludeId) {
|
||||
Long cnt = baseMapper.selectCount(Wrappers.<SmHotelDownstreamAppEntity>lambdaQuery()
|
||||
.eq(SmHotelDownstreamAppEntity::getAppKey, appKey)
|
||||
.ne(excludeId != null, SmHotelDownstreamAppEntity::getId, excludeId));
|
||||
return cnt != null && cnt > 0;
|
||||
}
|
||||
|
||||
private static String genSecret() {
|
||||
return IdUtil.fastSimpleUUID() + IdUtil.fastSimpleUUID().substring(0, 8);
|
||||
}
|
||||
|
||||
private void maskSecret(SmHotelDownstreamAppEntity app) {
|
||||
if (app == null) {
|
||||
return;
|
||||
}
|
||||
String s = app.getAppSecret();
|
||||
if (StrUtil.isBlank(s) || s.length() <= 6) {
|
||||
app.setAppSecretMasked("******");
|
||||
}
|
||||
else {
|
||||
app.setAppSecretMasked(s.substring(0, 2) + "****" + s.substring(s.length() - 4));
|
||||
}
|
||||
app.setAppSecret(null);
|
||||
}
|
||||
}
|
||||
+57
-78
@@ -5,8 +5,6 @@ import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
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.config.HotelDownstreamProperties;
|
||||
import com.bsd.pjcloud.supermarket.constant.SmHotelDownstreamProtocol;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelEntity;
|
||||
@@ -32,11 +30,9 @@ import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* 慧订订单履约:直连校验/创建/取消 + 通知补单 + operationOrder 回写。
|
||||
* 慧订订单履约:校验/创建/取消同步返回;创建时同步订上游,成败当场返回(不再异步 operationOrder)。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -46,12 +42,6 @@ public class SmHotelHuidingOrderServiceImpl implements SmHotelHuidingOrderServic
|
||||
|
||||
private static final DateTimeFormatter DAY = DateTimeFormatter.ISO_LOCAL_DATE;
|
||||
|
||||
private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(4, r -> {
|
||||
Thread t = new Thread(r, "sm-hotel-huiding-order");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
private final SmHotelChannelConfigService channelConfigService;
|
||||
|
||||
private final SmHotelOrderService hotelOrderService;
|
||||
@@ -149,7 +139,6 @@ public class SmHotelHuidingOrderServiceImpl implements SmHotelHuidingOrderServic
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> create(String partnerCode, JSONObject data) {
|
||||
Map<String, Object> outer = ok200();
|
||||
String channelOrderNo = StrUtil.trim(str(data.get("channelOrderNo")));
|
||||
@@ -209,11 +198,27 @@ public class SmHotelHuidingOrderServiceImpl implements SmHotelHuidingOrderServic
|
||||
order.setHuanqiPayload(data.toString());
|
||||
orderMapper.insert(order);
|
||||
|
||||
Long tenantId = TenantContextHolder.getTenantId() == null ? 1L : TenantContextHolder.getTenantId();
|
||||
scheduleBookAndConfirm(tenantId, partnerCode, order.getId(), channelOrderNo, mapped.orderAmount);
|
||||
|
||||
outer.put("msg", "下单成功,订单状态:待确认");
|
||||
outer.put("data", buildCreateData(order));
|
||||
// 同步订上游:当场返回成败,不走「先待确认再 operationOrder」中间态
|
||||
R<Boolean> book = hotelOrderService.bookUpstreamForExisting(order.getId());
|
||||
SmHotelOrderEntity latest = orderMapper.selectById(order.getId());
|
||||
if (latest == null) {
|
||||
return fail200("下单后订单丢失");
|
||||
}
|
||||
if (book == null || book.getCode() != 0 || !isBookConfirmed(latest)) {
|
||||
String failMsg = latest.getFailMsg();
|
||||
if (StrUtil.isBlank(failMsg)) {
|
||||
failMsg = book == null ? "上游下单失败" : StrUtil.blankToDefault(book.getMsg(), "上游下单失败");
|
||||
}
|
||||
if (!"UPSTREAM_FAIL".equals(latest.getOrderStatus()) && !"NEED_MANUAL".equals(latest.getOrderStatus())
|
||||
&& !"CANCELLED".equals(latest.getOrderStatus())) {
|
||||
latest.setOrderStatus("UPSTREAM_FAIL");
|
||||
latest.setFailMsg(StrUtil.maxLength(failMsg, 500));
|
||||
orderMapper.updateById(latest);
|
||||
}
|
||||
return fail200(failMsg);
|
||||
}
|
||||
outer.put("msg", "下单成功");
|
||||
outer.put("data", buildCreateData(latest));
|
||||
return outer;
|
||||
}
|
||||
|
||||
@@ -249,14 +254,8 @@ public class SmHotelHuidingOrderServiceImpl implements SmHotelHuidingOrderServic
|
||||
if (r == null || r.getCode() != 0) {
|
||||
return fail200(r == null ? "取消失败" : r.getMsg());
|
||||
}
|
||||
try {
|
||||
operationOrder(partnerCode, order.getChannelNo(), String.valueOf(order.getId()), 3, null);
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|{}|取消确认回写失败|channelNo={}|{}", TAG, channelOrderNo, e.getMessage());
|
||||
}
|
||||
Map<String, Object> outer = ok200();
|
||||
outer.put("msg", "申请退单成功");
|
||||
outer.put("msg", "取消成功");
|
||||
outer.put("data", true);
|
||||
return outer;
|
||||
}
|
||||
@@ -282,12 +281,19 @@ public class SmHotelHuidingOrderServiceImpl implements SmHotelHuidingOrderServic
|
||||
.eq(SmHotelOrderEntity::getChannelNo, channelNo)
|
||||
.last("limit 1"));
|
||||
if (exist != null) {
|
||||
Long tenantId = TenantContextHolder.getTenantId() == null ? 1L : TenantContextHolder.getTenantId();
|
||||
if (StrUtil.isBlank(exist.getPlatformNo()) && !"CANCELLED".equals(exist.getOrderStatus())) {
|
||||
scheduleBookAndConfirm(tenantId, partner, exist.getId(), exist.getChannelNo(), exist.getSellAmount());
|
||||
if (StrUtil.isBlank(exist.getPlatformNo()) && !"CANCELLED".equals(exist.getOrderStatus())
|
||||
&& !isBookConfirmed(exist)) {
|
||||
R<Boolean> book = hotelOrderService.bookUpstreamForExisting(exist.getId());
|
||||
SmHotelOrderEntity latest = orderMapper.selectById(exist.getId());
|
||||
if (book == null || book.getCode() != 0 || latest == null || !isBookConfirmed(latest)) {
|
||||
resp.put("code", 1);
|
||||
resp.put("message", latest != null && StrUtil.isNotBlank(latest.getFailMsg()) ? latest.getFailMsg()
|
||||
: (book == null ? "上游下单失败" : StrUtil.blankToDefault(book.getMsg(), "上游下单失败")));
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
resp.put("code", 0);
|
||||
resp.put("message", "本地已有订单,已触发履约");
|
||||
resp.put("message", "本地已有订单,履约完成");
|
||||
return resp;
|
||||
}
|
||||
// 本地没有:尝试查对方
|
||||
@@ -341,56 +347,18 @@ public class SmHotelHuidingOrderServiceImpl implements SmHotelHuidingOrderServic
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleBookAndConfirm(Long tenantId, String partnerCode, Long orderId, String childOrderCode,
|
||||
BigDecimal totalPrice) {
|
||||
EXECUTOR.execute(() -> TenantBroker.runAs(tenantId, tid -> {
|
||||
try {
|
||||
R<Boolean> r = hotelOrderService.bookUpstreamForExisting(orderId);
|
||||
SmHotelOrderEntity order = orderMapper.selectById(orderId);
|
||||
if (r != null && r.getCode() == 0) {
|
||||
operationOrder(partnerCode, childOrderCode,
|
||||
order == null ? String.valueOf(orderId)
|
||||
: StrUtil.blankToDefault(order.getPlatformNo(), String.valueOf(orderId)),
|
||||
1, totalPrice);
|
||||
}
|
||||
else {
|
||||
log.warn("|{}|上游下单失败,回写拒绝|orderId={}|msg={}|", TAG, orderId,
|
||||
r == null ? "null" : r.getMsg());
|
||||
operationOrder(partnerCode, childOrderCode, String.valueOf(orderId), 2, null);
|
||||
if (order != null) {
|
||||
order.setOrderStatus("NEED_MANUAL");
|
||||
order.setFailMsg(StrUtil.maxLength(r == null ? "上游下单失败" : r.getMsg(), 500));
|
||||
orderMapper.updateById(order);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|{}|异步履约失败|orderId={}|{}", TAG, orderId, e.getMessage());
|
||||
try {
|
||||
operationOrder(partnerCode, childOrderCode, String.valueOf(orderId), 2, null);
|
||||
}
|
||||
catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private void operationOrder(String partnerCode, String childOrderCode, String supplierOrderCode, int operationType,
|
||||
BigDecimal totalPrice) {
|
||||
HotelDownstreamProperties props = channelConfigService.resolveDownstreamRuntime(partnerCode);
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("key", HuidingDownstreamHotelAdapter.resolveKey(props));
|
||||
body.put("supplierId", props.getSupplierId());
|
||||
body.put("childOrderCode", childOrderCode);
|
||||
body.put("operationType", operationType);
|
||||
if (StrUtil.isNotBlank(supplierOrderCode) && (operationType == 1 || operationType == 3)) {
|
||||
body.put("supplierOrderCode", supplierOrderCode);
|
||||
/** 上游已明确订成(含本地库存单) */
|
||||
private static boolean isBookConfirmed(SmHotelOrderEntity order) {
|
||||
if (order == null) {
|
||||
return false;
|
||||
}
|
||||
if (totalPrice != null && operationType == 1) {
|
||||
body.put("totalPrice", totalPrice);
|
||||
if ("UPSTREAM_OK".equals(order.getOrderStatus()) || "CHECKED_IN".equals(order.getOrderStatus())) {
|
||||
return true;
|
||||
}
|
||||
downstreamClient.huidingOperationOrder(partnerCode, body);
|
||||
log.info("|{}|operationOrder|partner={}|child={}|type={}|", TAG, partnerCode, childOrderCode, operationType);
|
||||
return StrUtil.isNotBlank(order.getPlatformNo())
|
||||
&& !"UPSTREAM_FAIL".equals(order.getOrderStatus())
|
||||
&& !"NEED_MANUAL".equals(order.getOrderStatus())
|
||||
&& !"CANCELLED".equals(order.getOrderStatus());
|
||||
}
|
||||
|
||||
private String resolvePartnerByNotifyPass(String pass) {
|
||||
@@ -508,9 +476,20 @@ public class SmHotelHuidingOrderServiceImpl implements SmHotelHuidingOrderServic
|
||||
private static Map<String, Object> buildCreateData(SmHotelOrderEntity order) {
|
||||
Map<String, Object> data = new LinkedHashMap<>(6);
|
||||
data.put("orderNo", order.getId() == null ? order.getChannelNo() : String.valueOf(order.getId()));
|
||||
data.put("orderStatus", "1");
|
||||
data.put("orderStatusDesc", "待确认");
|
||||
if (isBookConfirmed(order)) {
|
||||
data.put("orderStatus", "3");
|
||||
data.put("orderStatusDesc", "待入住");
|
||||
}
|
||||
else if ("CANCELLED".equals(order.getOrderStatus())) {
|
||||
data.put("orderStatus", "5");
|
||||
data.put("orderStatusDesc", "已取消");
|
||||
}
|
||||
else {
|
||||
data.put("orderStatus", "1");
|
||||
data.put("orderStatusDesc", "待确认");
|
||||
}
|
||||
data.put("orderAmount", order.getSellAmount());
|
||||
data.put("platformNo", order.getPlatformNo());
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
+15
-1
@@ -904,7 +904,21 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
for (String code : codes) {
|
||||
Map<String, Object> row = new LinkedHashMap<>(4);
|
||||
row.put("partnerCode", code);
|
||||
row.put("label", code);
|
||||
HotelDownstreamProperties props = channelConfigService.resolveDownstreamRuntime(code);
|
||||
String protocol = props == null ? null : props.getProtocol();
|
||||
String protocolLabel = SmHotelDownstreamProtocol.label(protocol);
|
||||
row.put("protocol", SmHotelDownstreamProtocol.normalize(protocol));
|
||||
SmHotelChannelConfigEntity cfg = channelConfigService.getByChannel(code,
|
||||
com.bsd.pjcloud.supermarket.constant.SmHotelChannelDirection.DOWNSTREAM);
|
||||
String remark = cfg == null ? null : StrUtil.trim(cfg.getRemark());
|
||||
String label;
|
||||
if (StrUtil.isNotBlank(remark)) {
|
||||
label = StrUtil.isNotBlank(protocolLabel) ? protocolLabel + "·" + remark : remark;
|
||||
}
|
||||
else {
|
||||
label = StrUtil.isNotBlank(protocolLabel) ? protocolLabel + "·" + code : code;
|
||||
}
|
||||
row.put("label", label);
|
||||
options.add(row);
|
||||
}
|
||||
return R.ok(options);
|
||||
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
package com.bsd.pjcloud.supermarket.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelDailyEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelDownstreamAppEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelProductEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelRoomEntity;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelDailyMapper;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelMapper;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelProductMapper;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelRoomMapper;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelOpenApiService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelOrderService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SmHotelOpenApiServiceImpl implements SmHotelOpenApiService {
|
||||
|
||||
private static final DateTimeFormatter DAY = DateTimeFormatter.ISO_LOCAL_DATE;
|
||||
|
||||
private final SmHotelMapper hotelMapper;
|
||||
|
||||
private final SmHotelRoomMapper roomMapper;
|
||||
|
||||
private final SmHotelProductMapper productMapper;
|
||||
|
||||
private final SmHotelDailyMapper dailyMapper;
|
||||
|
||||
private final SmHotelOrderService hotelOrderService;
|
||||
|
||||
@Override
|
||||
public R<Map<String, Object>> hotelList(SmHotelDownstreamAppEntity app, Map<String, Object> body) {
|
||||
long current = toLong(body.get("current"), 1L);
|
||||
long size = Math.min(100L, toLong(body.get("size"), 20L));
|
||||
String keyword = StrUtil.trim(str(body.get("keyword")));
|
||||
String district = StrUtil.trim(str(body.get("district")));
|
||||
Page<SmHotelEntity> page = hotelMapper.selectPage(new Page<>(current, size),
|
||||
Wrappers.<SmHotelEntity>lambdaQuery()
|
||||
.eq(SmHotelEntity::getUpstreamStatus, "ACTIVE")
|
||||
.and(StrUtil.isNotBlank(keyword), w -> w.like(SmHotelEntity::getHotelName, keyword)
|
||||
.or()
|
||||
.like(SmHotelEntity::getHotelCode, keyword)
|
||||
.or()
|
||||
.like(SmHotelEntity::getAddress, keyword))
|
||||
.like(StrUtil.isNotBlank(district), SmHotelEntity::getDistrict, district)
|
||||
.orderByDesc(SmHotelEntity::getUpdateTime));
|
||||
List<Map<String, Object>> records = new ArrayList<>();
|
||||
for (SmHotelEntity h : page.getRecords()) {
|
||||
records.add(toHotelBrief(h));
|
||||
}
|
||||
Map<String, Object> data = new LinkedHashMap<>(8);
|
||||
data.put("records", records);
|
||||
data.put("total", page.getTotal());
|
||||
data.put("current", page.getCurrent());
|
||||
data.put("size", page.getSize());
|
||||
return R.ok(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<Map<String, Object>> hotelDetail(SmHotelDownstreamAppEntity app, Map<String, Object> body) {
|
||||
String hotelCode = StrUtil.trim(str(body.get("hotelCode")));
|
||||
if (StrUtil.isBlank(hotelCode)) {
|
||||
return R.failed("hotelCode 不能为空");
|
||||
}
|
||||
SmHotelEntity hotel = hotelMapper.selectOne(Wrappers.<SmHotelEntity>lambdaQuery()
|
||||
.eq(SmHotelEntity::getHotelCode, hotelCode)
|
||||
.last("limit 1"));
|
||||
if (hotel == null) {
|
||||
return R.failed("酒店不存在");
|
||||
}
|
||||
Map<String, Object> data = toHotelBrief(hotel);
|
||||
data.put("coverImage", hotel.getCoverImage());
|
||||
data.put("carouselImages", hotel.getCarouselImages());
|
||||
data.put("intro", hotel.getIntro());
|
||||
data.put("starLevel", hotel.getStarLevel());
|
||||
List<SmHotelRoomEntity> rooms = roomMapper.selectList(Wrappers.<SmHotelRoomEntity>lambdaQuery()
|
||||
.eq(SmHotelRoomEntity::getHotelId, hotel.getId())
|
||||
.orderByAsc(SmHotelRoomEntity::getRoomTypeCode));
|
||||
List<Map<String, Object>> roomList = new ArrayList<>();
|
||||
for (SmHotelRoomEntity r : rooms) {
|
||||
Map<String, Object> row = new LinkedHashMap<>(6);
|
||||
row.put("roomTypeCode", r.getRoomTypeCode());
|
||||
row.put("roomTypeName", r.getRoomTypeName());
|
||||
row.put("bedType", r.getBedType());
|
||||
row.put("capacity", r.getCapacity());
|
||||
roomList.add(row);
|
||||
}
|
||||
data.put("rooms", roomList);
|
||||
List<SmHotelProductEntity> products = productMapper.selectList(Wrappers.<SmHotelProductEntity>lambdaQuery()
|
||||
.eq(SmHotelProductEntity::getHotelId, hotel.getId())
|
||||
.orderByAsc(SmHotelProductEntity::getProductCode));
|
||||
List<Map<String, Object>> productList = new ArrayList<>();
|
||||
for (SmHotelProductEntity p : products) {
|
||||
Map<String, Object> row = new LinkedHashMap<>(8);
|
||||
row.put("productCode", p.getProductCode());
|
||||
row.put("productName", p.getProductName());
|
||||
row.put("roomTypeCode", p.getRoomTypeCode());
|
||||
row.put("priceCode", p.getPriceCode());
|
||||
row.put("mealType", p.getMealType());
|
||||
row.put("mealCount", p.getMealCount());
|
||||
row.put("sellPrice", p.getSellPrice());
|
||||
productList.add(row);
|
||||
}
|
||||
data.put("products", productList);
|
||||
return R.ok(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<Map<String, Object>> hotelDaily(SmHotelDownstreamAppEntity app, Map<String, Object> body) {
|
||||
String hotelCode = StrUtil.trim(str(body.get("hotelCode")));
|
||||
String productCode = StrUtil.trim(str(body.get("productCode")));
|
||||
LocalDate start = parseDay(str(body.get("startDate")));
|
||||
LocalDate end = parseDay(str(body.get("endDate")));
|
||||
if (StrUtil.isBlank(hotelCode) || start == null || end == null || end.isBefore(start)) {
|
||||
return R.failed("hotelCode/startDate/endDate 无效");
|
||||
}
|
||||
if (start.plusDays(90).isBefore(end)) {
|
||||
return R.failed("查询窗口最长 90 天");
|
||||
}
|
||||
SmHotelEntity hotel = hotelMapper.selectOne(Wrappers.<SmHotelEntity>lambdaQuery()
|
||||
.eq(SmHotelEntity::getHotelCode, hotelCode)
|
||||
.last("limit 1"));
|
||||
if (hotel == null) {
|
||||
return R.failed("酒店不存在");
|
||||
}
|
||||
List<SmHotelDailyEntity> dailies = dailyMapper.selectList(Wrappers.<SmHotelDailyEntity>lambdaQuery()
|
||||
.eq(SmHotelDailyEntity::getHotelId, hotel.getId())
|
||||
.eq(StrUtil.isNotBlank(productCode), SmHotelDailyEntity::getProductCode, productCode)
|
||||
.ge(SmHotelDailyEntity::getStayDate, start)
|
||||
.le(SmHotelDailyEntity::getStayDate, end)
|
||||
.orderByAsc(SmHotelDailyEntity::getProductCode)
|
||||
.orderByAsc(SmHotelDailyEntity::getStayDate)
|
||||
.last("limit 5000"));
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
for (SmHotelDailyEntity d : dailies) {
|
||||
Map<String, Object> row = new LinkedHashMap<>(10);
|
||||
row.put("productCode", d.getProductCode());
|
||||
row.put("stayDate", d.getStayDate() == null ? null : d.getStayDate().toString());
|
||||
row.put("sellPrice", d.getSellPrice());
|
||||
row.put("stock", d.getStock());
|
||||
row.put("sold", d.getSold());
|
||||
row.put("remain", d.getRemainStock());
|
||||
row.put("dailyStatus", d.getDailyStatus());
|
||||
list.add(row);
|
||||
}
|
||||
Map<String, Object> data = new LinkedHashMap<>(4);
|
||||
data.put("hotelCode", hotelCode);
|
||||
data.put("list", list);
|
||||
return R.ok(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<Map<String, Object>> orderCheck(SmHotelDownstreamAppEntity app, Map<String, Object> body) {
|
||||
Map<String, Object> checkBody = new LinkedHashMap<>();
|
||||
checkBody.put("extHotelId", body.get("hotelCode"));
|
||||
checkBody.put("extProductId", body.get("productCode"));
|
||||
checkBody.put("extApartmentId", body.get("roomTypeCode"));
|
||||
checkBody.put("checkIn", body.get("checkInDate"));
|
||||
checkBody.put("checkOut", body.get("checkOutDate"));
|
||||
checkBody.put("roomCnt", body.get("count") != null ? body.get("count") : body.get("quantity"));
|
||||
checkBody.put("price", body.get("totalAmount") != null ? body.get("totalAmount") : body.get("totalPrice"));
|
||||
return hotelOrderService.checkBookableForDownstream(checkBody);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<JSONObject> orderCreate(SmHotelDownstreamAppEntity app, Map<String, Object> body) {
|
||||
// 保证订单来源写入伙伴编码
|
||||
R<JSONObject> r = hotelOrderService.createFromDownstream(app.getAppKey(), body);
|
||||
return r;
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<JSONObject> orderCancel(SmHotelDownstreamAppEntity app, Map<String, Object> body) {
|
||||
R<JSONObject> r = hotelOrderService.cancelFromDownstream(app.getAppKey(), body);
|
||||
return r;
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<JSONObject> orderQuery(SmHotelDownstreamAppEntity app, Map<String, Object> body) {
|
||||
return hotelOrderService.queryFromDownstream(app.getAppKey(), body);
|
||||
}
|
||||
|
||||
private Map<String, Object> toHotelBrief(SmHotelEntity h) {
|
||||
Map<String, Object> row = new LinkedHashMap<>(12);
|
||||
row.put("hotelCode", h.getHotelCode());
|
||||
row.put("hotelName", h.getHotelName());
|
||||
row.put("groupCode", h.getGroupCode());
|
||||
row.put("address", h.getAddress());
|
||||
row.put("district", h.getDistrict());
|
||||
row.put("bizArea", h.getBizArea());
|
||||
row.put("longitude", h.getLongitude());
|
||||
row.put("latitude", h.getLatitude());
|
||||
row.put("dataSource", h.getSheetName());
|
||||
row.put("status", h.getUpstreamStatus());
|
||||
return row;
|
||||
}
|
||||
|
||||
private static String str(Object o) {
|
||||
return o == null ? null : String.valueOf(o);
|
||||
}
|
||||
|
||||
private static long toLong(Object o, long def) {
|
||||
if (o == null) {
|
||||
return def;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(String.valueOf(o));
|
||||
}
|
||||
catch (Exception e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
private static LocalDate parseDay(String s) {
|
||||
if (StrUtil.isBlank(s)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(s.trim(), DAY);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+214
-11
@@ -8,9 +8,11 @@ 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.constant.SmHotelDataSource;
|
||||
import com.bsd.pjcloud.supermarket.constant.SmHotelDownstreamProtocol;
|
||||
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.downstream.DownstreamOpenCallbackClient;
|
||||
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;
|
||||
@@ -98,6 +100,8 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
|
||||
private final RemoterThirdAppPlusPayService remoterThirdAppPlusPayService;
|
||||
|
||||
private final DownstreamOpenCallbackClient openCallbackClient;
|
||||
|
||||
private final SmHotelRoomMapper roomMapper;
|
||||
|
||||
@Override
|
||||
@@ -106,6 +110,10 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
.like(StrUtil.isNotBlank(query.getChannelNo()), SmHotelOrderEntity::getChannelNo, query.getChannelNo())
|
||||
.like(StrUtil.isNotBlank(query.getPlatformNo()), SmHotelOrderEntity::getPlatformNo,
|
||||
query.getPlatformNo())
|
||||
.eq(StrUtil.isNotBlank(query.getPartnerCode()), SmHotelOrderEntity::getPartnerCode,
|
||||
query.getPartnerCode())
|
||||
.eq(query.getDownstreamAppId() != null, SmHotelOrderEntity::getDownstreamAppId,
|
||||
query.getDownstreamAppId())
|
||||
.eq(StrUtil.isNotBlank(query.getOrderStatus()), SmHotelOrderEntity::getOrderStatus,
|
||||
query.getOrderStatus())
|
||||
.eq(StrUtil.isNotBlank(query.getBookStatus()), SmHotelOrderEntity::getBookStatus, query.getBookStatus())
|
||||
@@ -123,6 +131,50 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> listOrderSourceOptions() {
|
||||
List<Map<String, Object>> options = new ArrayList<>();
|
||||
java.util.LinkedHashSet<String> seen = new java.util.LinkedHashSet<>();
|
||||
for (SmHotelChannelConfigEntity cfg : channelConfigService.listDownstreamPartners()) {
|
||||
if (cfg == null || StrUtil.isBlank(cfg.getChannel()) || !"1".equals(cfg.getEnabled())) {
|
||||
continue;
|
||||
}
|
||||
String code = cfg.getChannel().trim();
|
||||
if (!seen.add(code)) {
|
||||
continue;
|
||||
}
|
||||
String protocol = SmHotelDownstreamProtocol.normalize(cfg.getProtocol());
|
||||
String protocolLabel = SmHotelDownstreamProtocol.label(protocol);
|
||||
String remark = StrUtil.trim(cfg.getRemark());
|
||||
String label = StrUtil.isNotBlank(remark) ? protocolLabel + "·" + remark : protocolLabel + "·" + code;
|
||||
Map<String, Object> row = new LinkedHashMap<>(6);
|
||||
row.put("partnerCode", code);
|
||||
row.put("protocol", protocol);
|
||||
row.put("label", label);
|
||||
row.put("type", "channel");
|
||||
options.add(row);
|
||||
}
|
||||
List<SmHotelDownstreamAppEntity> apps = appMapper.selectList(Wrappers.<SmHotelDownstreamAppEntity>lambdaQuery()
|
||||
.eq(SmHotelDownstreamAppEntity::getStatus, "1")
|
||||
.orderByAsc(SmHotelDownstreamAppEntity::getAppName)
|
||||
.orderByAsc(SmHotelDownstreamAppEntity::getAppKey));
|
||||
for (SmHotelDownstreamAppEntity app : apps) {
|
||||
String code = StrUtil.blankToDefault(StrUtil.trim(app.getPartnerCode()), StrUtil.trim(app.getAppKey()));
|
||||
if (StrUtil.isBlank(code) || !seen.add(code)) {
|
||||
continue;
|
||||
}
|
||||
String name = StrUtil.blankToDefault(app.getAppName(), code);
|
||||
Map<String, Object> row = new LinkedHashMap<>(6);
|
||||
row.put("partnerCode", code);
|
||||
row.put("protocol", "open");
|
||||
row.put("label", "开放·" + name);
|
||||
row.put("type", "open");
|
||||
row.put("downstreamAppId", app.getId());
|
||||
options.add(row);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<SmHotelOrderEntity> pageMyAppOrders(Page<SmHotelOrderEntity> page, PjcloudUser user,
|
||||
String orderStatus) {
|
||||
@@ -154,7 +206,8 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页列表展示字段:酒店名称、数据来源(本地/上游)、channelNo 推断下单来源。
|
||||
* 分页列表展示字段:酒店名称、数据来源、订单来源(App/后台/分销伙伴协议+编码/下游应用)。
|
||||
* 酒店数据来源 ≠ 订单销售来源:同一上游(如飞猪)可被多家下游售出,靠 partnerCode / downstreamAppId 区分。
|
||||
*/
|
||||
private void fillOrderListDisplay(List<SmHotelOrderEntity> records) {
|
||||
if (records == null || records.isEmpty()) {
|
||||
@@ -188,6 +241,51 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
}
|
||||
}
|
||||
}
|
||||
// 渠道对接配置:协议 + 展示名(优先备注)
|
||||
Map<String, String> partnerProtocolMap = new HashMap<>();
|
||||
Map<String, String> partnerNameMap = new HashMap<>();
|
||||
for (SmHotelChannelConfigEntity cfg : channelConfigService.listDownstreamPartners()) {
|
||||
if (cfg == null || StrUtil.isBlank(cfg.getChannel())) {
|
||||
continue;
|
||||
}
|
||||
String code = cfg.getChannel().trim();
|
||||
String protocol = SmHotelDownstreamProtocol.normalize(cfg.getProtocol());
|
||||
partnerProtocolMap.put(code, protocol);
|
||||
String name = StrUtil.trim(cfg.getRemark());
|
||||
if (StrUtil.isBlank(name)) {
|
||||
name = SmHotelDownstreamProtocol.label(protocol) + "·" + code;
|
||||
}
|
||||
partnerNameMap.put(code, name);
|
||||
}
|
||||
// 开放应用:按 id / partnerCode / appKey 映射名称
|
||||
List<Long> appIds = records.stream()
|
||||
.map(SmHotelOrderEntity::getDownstreamAppId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
Map<Long, SmHotelDownstreamAppEntity> appById = new HashMap<>();
|
||||
if (!appIds.isEmpty()) {
|
||||
for (SmHotelDownstreamAppEntity app : appMapper.selectByIds(appIds)) {
|
||||
appById.put(app.getId(), app);
|
||||
indexOpenAppNames(partnerNameMap, partnerProtocolMap, app);
|
||||
}
|
||||
}
|
||||
List<String> partnerCodes = records.stream()
|
||||
.map(SmHotelOrderEntity::getPartnerCode)
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.map(String::trim)
|
||||
.filter(c -> !partnerNameMap.containsKey(c))
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (!partnerCodes.isEmpty()) {
|
||||
List<SmHotelDownstreamAppEntity> apps = appMapper.selectList(Wrappers.<SmHotelDownstreamAppEntity>lambdaQuery()
|
||||
.and(w -> w.in(SmHotelDownstreamAppEntity::getPartnerCode, partnerCodes)
|
||||
.or()
|
||||
.in(SmHotelDownstreamAppEntity::getAppKey, partnerCodes)));
|
||||
for (SmHotelDownstreamAppEntity app : apps) {
|
||||
indexOpenAppNames(partnerNameMap, partnerProtocolMap, app);
|
||||
}
|
||||
}
|
||||
for (SmHotelOrderEntity o : records) {
|
||||
SmHotelEntity hotel = o.getHotelId() != null ? hotelById.get(o.getHotelId()) : null;
|
||||
if (hotel == null && StrUtil.isNotBlank(o.getHotelCode())) {
|
||||
@@ -197,20 +295,107 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
o.setHotelName(hotel.getHotelName());
|
||||
o.setHotelDataSource(upstreamGatewayRegistry.resolveChannelKey(hotel.getSheetName()));
|
||||
}
|
||||
String channelNo = o.getChannelNo();
|
||||
if (StrUtil.startWithIgnoreCase(channelNo, "APP")) {
|
||||
o.setOrderSource("APP");
|
||||
SmHotelDownstreamAppEntity app = o.getDownstreamAppId() == null ? null
|
||||
: appById.get(o.getDownstreamAppId());
|
||||
if (app != null) {
|
||||
o.setDownstreamAppName(StrUtil.blankToDefault(app.getAppName(), app.getAppKey()));
|
||||
}
|
||||
else if (StrUtil.startWithIgnoreCase(channelNo, "ADMIN")) {
|
||||
o.setOrderSource("ADMIN");
|
||||
String partner = StrUtil.trim(o.getPartnerCode());
|
||||
if (StrUtil.isNotBlank(partner)) {
|
||||
o.setPartnerProtocol(partnerProtocolMap.get(partner));
|
||||
}
|
||||
else if (StrUtil.startWithIgnoreCase(o.getPlatformNo(), "LOCAL")) {
|
||||
o.setOrderSource("LOCAL");
|
||||
else if (app != null) {
|
||||
o.setPartnerProtocol("open");
|
||||
}
|
||||
fillOrderSourceFields(o, app, partnerNameMap);
|
||||
}
|
||||
}
|
||||
|
||||
private static void indexOpenAppNames(Map<String, String> partnerNameMap,
|
||||
Map<String, String> partnerProtocolMap, SmHotelDownstreamAppEntity app) {
|
||||
if (app == null) {
|
||||
return;
|
||||
}
|
||||
String display = StrUtil.blankToDefault(StrUtil.trim(app.getAppName()),
|
||||
StrUtil.blankToDefault(app.getPartnerCode(), app.getAppKey()));
|
||||
if (StrUtil.isNotBlank(app.getPartnerCode())) {
|
||||
partnerNameMap.putIfAbsent(app.getPartnerCode().trim(), display);
|
||||
partnerProtocolMap.putIfAbsent(app.getPartnerCode().trim(), "open");
|
||||
}
|
||||
if (StrUtil.isNotBlank(app.getAppKey())) {
|
||||
partnerNameMap.putIfAbsent(app.getAppKey().trim(), display);
|
||||
partnerProtocolMap.putIfAbsent(app.getAppKey().trim(), "open");
|
||||
}
|
||||
}
|
||||
|
||||
/** 解析订单销售来源(与酒店 sheet_name 数据来源分离);不同下游显示各自名称。 */
|
||||
private void fillOrderSourceFields(SmHotelOrderEntity o, SmHotelDownstreamAppEntity app,
|
||||
Map<String, String> partnerNameMap) {
|
||||
String channelNo = o.getChannelNo();
|
||||
String partner = StrUtil.trim(o.getPartnerCode());
|
||||
String protocol = o.getPartnerProtocol();
|
||||
if (StrUtil.startWithIgnoreCase(channelNo, "APP") && StrUtil.isBlank(partner) && app == null) {
|
||||
o.setOrderSource("APP");
|
||||
o.setOrderSourceLabel("App");
|
||||
return;
|
||||
}
|
||||
if (StrUtil.startWithIgnoreCase(channelNo, "ADMIN") && StrUtil.isBlank(partner) && app == null) {
|
||||
o.setOrderSource("ADMIN");
|
||||
o.setOrderSourceLabel("后台");
|
||||
return;
|
||||
}
|
||||
if (StrUtil.startWithIgnoreCase(o.getPlatformNo(), "LOCAL") && StrUtil.isBlank(partner) && app == null) {
|
||||
o.setOrderSource("LOCAL");
|
||||
o.setOrderSourceLabel("本地确认");
|
||||
return;
|
||||
}
|
||||
if (StrUtil.isNotBlank(partner) || app != null) {
|
||||
o.setOrderSource("DOWNSTREAM");
|
||||
String appName = app == null ? null : StrUtil.blankToDefault(app.getAppName(), app.getAppKey());
|
||||
String named = StrUtil.isNotBlank(partner) ? partnerNameMap.get(partner) : null;
|
||||
if (StrUtil.isBlank(named) && StrUtil.isNotBlank(appName)) {
|
||||
named = appName;
|
||||
}
|
||||
if (StrUtil.isBlank(protocol) && (app != null || "open".equals(o.getPartnerProtocol()))) {
|
||||
protocol = "open";
|
||||
o.setPartnerProtocol("open");
|
||||
}
|
||||
String protocolLabel = StrUtil.isNotBlank(protocol) ? protocolDisplayLabel(protocol) : null;
|
||||
if (StrUtil.isNotBlank(named)) {
|
||||
// 名称里已含协议前缀则不再重复拼
|
||||
if (StrUtil.isNotBlank(protocolLabel) && !named.contains(protocolLabel)
|
||||
&& !named.startsWith(protocolLabel)) {
|
||||
o.setOrderSourceLabel(protocolLabel + "·" + named);
|
||||
}
|
||||
else {
|
||||
o.setOrderSourceLabel(named);
|
||||
}
|
||||
}
|
||||
else if (StrUtil.isNotBlank(protocolLabel) && StrUtil.isNotBlank(partner)) {
|
||||
o.setOrderSourceLabel(protocolLabel + "·" + partner);
|
||||
}
|
||||
else if (StrUtil.isNotBlank(partner)) {
|
||||
o.setOrderSourceLabel(partner);
|
||||
}
|
||||
else {
|
||||
o.setOrderSource(StrUtil.isBlank(channelNo) ? "-" : "DOWNSTREAM");
|
||||
o.setOrderSourceLabel(StrUtil.blankToDefault(protocolLabel, "下游"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (StrUtil.isNotBlank(channelNo)) {
|
||||
o.setOrderSource("DOWNSTREAM");
|
||||
o.setOrderSourceLabel("下游");
|
||||
return;
|
||||
}
|
||||
o.setOrderSource("-");
|
||||
o.setOrderSourceLabel("-");
|
||||
}
|
||||
|
||||
private static String protocolDisplayLabel(String protocol) {
|
||||
if ("open".equalsIgnoreCase(protocol)) {
|
||||
return "开放";
|
||||
}
|
||||
return SmHotelDownstreamProtocol.label(protocol);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -406,6 +591,9 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
|
||||
SmHotelOrderEntity order = new SmHotelOrderEntity();
|
||||
order.setDownstreamAppId(app.getId());
|
||||
// 订单销售来源:开放应用 partnerCode(空则 appKey),与环启/慧订分销伙伴同一列
|
||||
order.setPartnerCode(StrUtil.blankToDefault(StrUtil.trim(app.getPartnerCode()),
|
||||
StrUtil.blankToDefault(app.getAppKey(), app.getAppName())));
|
||||
order.setHotelId(hotel.getId());
|
||||
order.setChannelNo(channelNo);
|
||||
order.setGroupCode(groupCode);
|
||||
@@ -445,7 +633,9 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
|
||||
// 本地酒店:扣本地日历库存,不走上游
|
||||
if (localHotel) {
|
||||
return finishLocalOrder(order, product);
|
||||
R<JSONObject> local = finishLocalOrder(order, product);
|
||||
notifyOpenOrderCallback(order);
|
||||
return local;
|
||||
}
|
||||
|
||||
Optional<HotelUpstreamOrderGateway> gatewayOpt = resolveUpstreamGateway(hotel, order, false);
|
||||
@@ -462,6 +652,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
order.setOrderStatus("UPSTREAM_FAIL");
|
||||
order.setFailMsg(StrUtil.sub(availMsg, 0, 500));
|
||||
baseMapper.updateById(order);
|
||||
notifyOpenOrderCallback(order);
|
||||
return R.failed(availMsg);
|
||||
}
|
||||
}
|
||||
@@ -474,6 +665,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
HotelUpstreamOrderResult call = invokeCreateOrder(gateway, order, upstreamBody, "CREATE");
|
||||
applyCreateResult(order, call, upstreamBody);
|
||||
baseMapper.updateById(order);
|
||||
notifyOpenOrderCallback(order);
|
||||
JSONObject out = buildOut(order);
|
||||
if ("UPSTREAM_FAIL".equals(order.getOrderStatus()) && !COMP_WAITING.equals(order.getCompensateStatus())) {
|
||||
return R.failed(StrUtil.blankToDefault(order.getFailMsg(), "上游下单失败"));
|
||||
@@ -2010,6 +2202,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
order.setCompensateStatus(COMP_DONE);
|
||||
order.setNextCompensateTime(null);
|
||||
baseMapper.updateById(order);
|
||||
notifyOpenOrderCallback(order);
|
||||
return R.ok(buildOut(order), "本地订单已取消并回补库存");
|
||||
}
|
||||
if (StrUtil.isBlank(order.getPlatformNo())) {
|
||||
@@ -2019,7 +2212,9 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
if (cancelR == null || !cancelR.isOk()) {
|
||||
return R.failed(cancelR == null ? "上游取消失败" : cancelR.getMsg());
|
||||
}
|
||||
return R.ok(buildOut(baseMapper.selectById(order.getId())));
|
||||
SmHotelOrderEntity latest = baseMapper.selectById(order.getId());
|
||||
notifyOpenOrderCallback(latest);
|
||||
return R.ok(buildOut(latest));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -2392,6 +2587,14 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
return dailyPriceList;
|
||||
}
|
||||
|
||||
/** 开放应用订单状态回调(未配置回调则跳过) */
|
||||
private void notifyOpenOrderCallback(SmHotelOrderEntity order) {
|
||||
if (order == null || order.getDownstreamAppId() == null || openCallbackClient == null) {
|
||||
return;
|
||||
}
|
||||
openCallbackClient.notifyOrderStatusAsync(order.getDownstreamAppId(), order);
|
||||
}
|
||||
|
||||
private SmHotelDownstreamAppEntity requireApp(String appKey) {
|
||||
if (StrUtil.isBlank(appKey)) {
|
||||
throw new IllegalArgumentException("缺少 appKey");
|
||||
|
||||
Reference in new issue
Block a user