酒店,消息推送
This commit is contained in:
1 parent
588f34bef5
commit
47a1fb2a27
18 files changed
+1147
-6
No files matched your search
@@ -66,6 +66,9 @@ node_modules
|
||||
### 充值导入测试脚本(含 Excel 原始数据与本地配置) ###
|
||||
scripts/recharge-import/
|
||||
|
||||
#文档#
|
||||
*.md
|
||||
|
||||
### AI 辅助工具 ###
|
||||
.video_agent
|
||||
.mimosa
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
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_avail_sub")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "酒店有房通知订阅")
|
||||
public class SmHotelAvailSubEntity extends Model<SmHotelAvailSubEntity> {
|
||||
|
||||
public static final String STATUS_ACTIVE = "ACTIVE";
|
||||
public static final String STATUS_NOTIFIED = "NOTIFIED";
|
||||
public static final String STATUS_CANCELLED = "CANCELLED";
|
||||
|
||||
/** 指定入住离店区间,整段可订才通知 */
|
||||
public static final String WATCH_RANGE = "RANGE";
|
||||
|
||||
/** 不限日期:近窗内任意一晚可订即通知 */
|
||||
public static final String WATCH_ANY = "ANY";
|
||||
|
||||
/** ANY 模式落库占位入住日(不参与业务判定) */
|
||||
public static final LocalDate ANY_SENTINEL_IN = LocalDate.of(1900, 1, 1);
|
||||
|
||||
/** ANY 模式落库占位离店日(不参与业务判定) */
|
||||
public static final LocalDate ANY_SENTINEL_OUT = LocalDate.of(1900, 1, 2);
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
private Long tenantId;
|
||||
|
||||
private Long userId;
|
||||
|
||||
private Long hotelId;
|
||||
|
||||
private String productCode;
|
||||
|
||||
private LocalDate checkIn;
|
||||
|
||||
private LocalDate checkOut;
|
||||
|
||||
private Integer rooms;
|
||||
|
||||
@Schema(description = "RANGE指定日期 / ANY不限日期")
|
||||
private String watchMode;
|
||||
|
||||
@Schema(description = "ACTIVE/NOTIFIED/CANCELLED")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "推送幂等版本")
|
||||
private Integer notifyVersion;
|
||||
|
||||
private String hotelName;
|
||||
|
||||
private String productName;
|
||||
|
||||
private LocalDateTime notifiedTime;
|
||||
|
||||
@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;
|
||||
}
|
||||
+10
@@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@@ -37,6 +38,15 @@ public class SmHotelRoomEntity extends Model<SmHotelRoomEntity> {
|
||||
|
||||
private Integer roomNumber;
|
||||
|
||||
@Schema(description = "楼层(如 3-5层)")
|
||||
private String floorInfo;
|
||||
|
||||
@Schema(description = "面积(㎡)")
|
||||
private BigDecimal areaSqm;
|
||||
|
||||
@Schema(description = "窗型:有窗/无窗/部分有窗/其他")
|
||||
private String windowType;
|
||||
|
||||
private String rawJson;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
|
||||
+83
@@ -11,6 +11,7 @@ import com.bsd.pjcloud.common.api.encrypt.annotation.crypto.ApiCryptoAes;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.security.service.PjcloudUser;
|
||||
import com.bsd.pjcloud.common.security.util.SecurityUtils;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelAvailSubEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelDailyEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelOrderEntity;
|
||||
@@ -21,6 +22,7 @@ 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.mapper.SmHotelUserSearchPrefMapper;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelAvailabilityNotifyService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelMidService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelOrderService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelService;
|
||||
@@ -120,6 +122,8 @@ public class SmHotelAppController {
|
||||
|
||||
private final SmHotelProductMapper productMapper;
|
||||
|
||||
private final SmHotelAvailabilityNotifyService availabilityNotifyService;
|
||||
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
@Operation(summary = "列表筛选项(区域/星级/设施)")
|
||||
@@ -879,6 +883,73 @@ public class SmHotelAppController {
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "订阅有房通知")
|
||||
@PostMapping("/availability-subscribe")
|
||||
public R<SmHotelAvailSubEntity> availabilitySubscribe(@RequestBody Map<String, Object> body) {
|
||||
PjcloudUser user = SecurityUtils.getUser();
|
||||
if (user == null) {
|
||||
return R.failed("请先登录");
|
||||
}
|
||||
if (body == null) {
|
||||
body = Map.of();
|
||||
}
|
||||
Long hotelId = parseLong(body.get("hotelId"));
|
||||
String productCode = str(body.get("productCode"));
|
||||
LocalDate checkIn = parseDay(str(body.get("checkIn")));
|
||||
LocalDate checkOut = parseDay(str(body.get("checkOut")));
|
||||
Integer rooms = clampInt(body.get("rooms"), 1, 20, 1);
|
||||
String watchMode = str(body.get("watchMode"));
|
||||
return availabilityNotifyService.subscribe(user.getId(), hotelId, productCode, checkIn, checkOut, rooms,
|
||||
watchMode);
|
||||
}
|
||||
|
||||
@Operation(summary = "取消有房通知订阅")
|
||||
@PostMapping("/availability-unsubscribe")
|
||||
public R<Boolean> availabilityUnsubscribe(@RequestBody Map<String, Object> body) {
|
||||
PjcloudUser user = SecurityUtils.getUser();
|
||||
if (user == null) {
|
||||
return R.failed("请先登录");
|
||||
}
|
||||
if (body == null) {
|
||||
body = Map.of();
|
||||
}
|
||||
Long id = parseLong(body.get("id"));
|
||||
Long hotelId = parseLong(body.get("hotelId"));
|
||||
String productCode = str(body.get("productCode"));
|
||||
LocalDate checkIn = parseDay(str(body.get("checkIn")));
|
||||
LocalDate checkOut = parseDay(str(body.get("checkOut")));
|
||||
Integer rooms = clampInt(body.get("rooms"), 1, 20, 1);
|
||||
String watchMode = str(body.get("watchMode"));
|
||||
return availabilityNotifyService.unsubscribe(user.getId(), id, hotelId, productCode, checkIn, checkOut, rooms,
|
||||
watchMode);
|
||||
}
|
||||
|
||||
@Operation(summary = "我的有房通知订阅")
|
||||
@GetMapping("/availability-subscriptions")
|
||||
public R<Page<SmHotelAvailSubEntity>> availabilitySubscriptions(@ParameterObject Page<?> page) {
|
||||
PjcloudUser user = SecurityUtils.getUser();
|
||||
if (user == null) {
|
||||
return R.failed("请先登录");
|
||||
}
|
||||
return R.ok(availabilityNotifyService.pageMine(page, user.getId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "有房通知订阅状态")
|
||||
@GetMapping("/availability-subscribe/status")
|
||||
public R<Map<String, Object>> availabilitySubscribeStatus(
|
||||
@RequestParam Long hotelId,
|
||||
@RequestParam String productCode,
|
||||
@RequestParam String checkIn,
|
||||
@RequestParam String checkOut,
|
||||
@RequestParam(required = false) Integer rooms) {
|
||||
PjcloudUser user = SecurityUtils.getUser();
|
||||
if (user == null) {
|
||||
return R.failed("请先登录");
|
||||
}
|
||||
return R.ok(availabilityNotifyService.status(user.getId(), hotelId, productCode,
|
||||
parseDay(checkIn), parseDay(checkOut), rooms));
|
||||
}
|
||||
|
||||
@Operation(summary = "酒店日历分页")
|
||||
@GetMapping("/detail/{id}/daily")
|
||||
public R<Page<SmHotelDailyEntity>> daily(@PathVariable Long id,
|
||||
@@ -1410,4 +1481,16 @@ public class SmHotelAppController {
|
||||
private static String str(Object o) {
|
||||
return o == null ? null : String.valueOf(o);
|
||||
}
|
||||
|
||||
private static Long parseLong(Object raw) {
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.valueOf(String.valueOf(raw).trim());
|
||||
}
|
||||
catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -7,6 +7,7 @@ import com.bsd.pjcloud.supermarket.entity.SmHotelPriceConfigEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelProductEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelRoomEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelSyncLogEntity;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelAvailabilityNotifyService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelDeptService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelMidService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelPriceConfigService;
|
||||
@@ -42,6 +43,8 @@ public class SmHotelMidController {
|
||||
|
||||
private final SmHotelChannelDeptService channelDeptService;
|
||||
|
||||
private final SmHotelAvailabilityNotifyService availabilityNotifyService;
|
||||
|
||||
@Operation(summary = "查询全局售价调整配置(加价/折扣)")
|
||||
@GetMapping("/priceConfig")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotelPrice_view')")
|
||||
@@ -300,6 +303,14 @@ public class SmHotelMidController {
|
||||
return midService.batchLocalDaily(hotelId, req);
|
||||
}
|
||||
|
||||
@SysLog("手动扫描有房通知订阅")
|
||||
@Operation(summary = "立即扫描该酒店 ACTIVE 有房订阅并尝试推送(联调/补推)")
|
||||
@PostMapping("/local/{hotelId}/avail-notify/scan")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
public R<Map<String, Object>> scanAvailNotify(@PathVariable Long hotelId) {
|
||||
return R.ok(availabilityNotifyService.scanNow(hotelId));
|
||||
}
|
||||
|
||||
@SysLog("本地酒店删除日历")
|
||||
@DeleteMapping("/local/{hotelId}/daily/{dailyId}")
|
||||
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_edit')")
|
||||
|
||||
+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.SmHotelAvailSubEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface SmHotelAvailSubMapper extends PjcloudBaseMapper<SmHotelAvailSubEntity> {
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.bsd.pjcloud.supermarket.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelAvailSubEntity;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 酒店有房通知:用户订阅 CRUD + 日历变更后扫描推送。
|
||||
*/
|
||||
public interface SmHotelAvailabilityNotifyService {
|
||||
|
||||
/**
|
||||
* 订阅有房通知。
|
||||
*
|
||||
* @param watchMode RANGE=指定日期(须传 checkIn/checkOut);ANY=不限日期有房就通知
|
||||
*/
|
||||
R<SmHotelAvailSubEntity> subscribe(Long userId, Long hotelId, String productCode,
|
||||
LocalDate checkIn, LocalDate checkOut, Integer rooms, String watchMode);
|
||||
|
||||
/**
|
||||
* 取消订阅(优先按 id;否则按同键)。
|
||||
*/
|
||||
R<Boolean> unsubscribe(Long userId, Long id, Long hotelId, String productCode,
|
||||
LocalDate checkIn, LocalDate checkOut, Integer rooms, String watchMode);
|
||||
|
||||
/**
|
||||
* 当前用户订阅分页。
|
||||
*/
|
||||
Page<SmHotelAvailSubEntity> pageMine(Page<?> page, Long userId);
|
||||
|
||||
/**
|
||||
* 是否已订阅:匹配指定日期 RANGE,或同产品同间数的 ANY。
|
||||
*/
|
||||
Map<String, Object> status(Long userId, Long hotelId, String productCode,
|
||||
LocalDate checkIn, LocalDate checkOut, Integer rooms);
|
||||
|
||||
/**
|
||||
* 酒店日历/库存变化后防抖扫描 ACTIVE 订阅并推送。
|
||||
*/
|
||||
void scheduleCheckAfterHotelChanged(Long hotelId);
|
||||
|
||||
/**
|
||||
* 立即扫描指定酒店的 ACTIVE 有房订阅(运维/联调手动触发)。
|
||||
*/
|
||||
Map<String, Object> scanNow(Long hotelId);
|
||||
}
|
||||
+609
@@ -0,0 +1,609 @@
|
||||
package com.bsd.pjcloud.supermarket.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
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.common.data.tenant.TenantBroker;
|
||||
import com.bsd.pjcloud.common.data.tenant.TenantContextHolder;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelAvailSubEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelDailyEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelEntity;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelProductEntity;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelAvailSubMapper;
|
||||
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.service.SmHotelAvailabilityNotifyService;
|
||||
import com.bsd.pjcloud.thirdparty.api.feign.RemoterThirdAppMessageService;
|
||||
import com.bsd.pjcloud.thirdparty.dto.appmessage.HotelRoomAvailableEventDto;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 有房通知订阅与日历变更扫描推送。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SmHotelAvailabilityNotifyServiceImpl implements SmHotelAvailabilityNotifyService {
|
||||
|
||||
private static final DateTimeFormatter DAY = DateTimeFormatter.ISO_LOCAL_DATE;
|
||||
|
||||
private static final long DEBOUNCE_MS = 800L;
|
||||
|
||||
/** ANY 模式扫描:今天起多少天内任意一晚可订即通知 */
|
||||
private static final int ANY_LOOKAHEAD_DAYS = 30;
|
||||
|
||||
private final SmHotelAvailSubMapper availSubMapper;
|
||||
|
||||
private final SmHotelMapper hotelMapper;
|
||||
|
||||
private final SmHotelProductMapper productMapper;
|
||||
|
||||
private final SmHotelDailyMapper dailyMapper;
|
||||
|
||||
private final RemoterThirdAppMessageService remoteAppMessageService;
|
||||
|
||||
private final ConcurrentHashMap<Long, ScheduledFuture<?>> pendingByHotel = new ConcurrentHashMap<>();
|
||||
|
||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "sm-hotel-avail-notify");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<SmHotelAvailSubEntity> subscribe(Long userId, Long hotelId, String productCode,
|
||||
LocalDate checkIn, LocalDate checkOut, Integer rooms, String watchMode) {
|
||||
if (userId == null || hotelId == null || StrUtil.isBlank(productCode)) {
|
||||
return R.failed("订阅参数不完整");
|
||||
}
|
||||
String mode = normalizeWatchMode(watchMode);
|
||||
boolean anyMode = SmHotelAvailSubEntity.WATCH_ANY.equals(mode);
|
||||
LocalDate in;
|
||||
LocalDate out;
|
||||
if (anyMode) {
|
||||
in = SmHotelAvailSubEntity.ANY_SENTINEL_IN;
|
||||
out = SmHotelAvailSubEntity.ANY_SENTINEL_OUT;
|
||||
}
|
||||
else {
|
||||
if (checkIn == null || checkOut == null) {
|
||||
return R.failed("请选择入住和离店日期");
|
||||
}
|
||||
if (!checkOut.isAfter(checkIn)) {
|
||||
return R.failed("离店日须晚于入住日");
|
||||
}
|
||||
in = checkIn;
|
||||
out = checkOut;
|
||||
}
|
||||
int need = normalizeRooms(rooms);
|
||||
SmHotelEntity hotel = hotelMapper.selectById(hotelId);
|
||||
if (hotel == null) {
|
||||
return R.failed("酒店不存在");
|
||||
}
|
||||
SmHotelProductEntity product = productMapper.selectOne(Wrappers.<SmHotelProductEntity>lambdaQuery()
|
||||
.eq(SmHotelProductEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelProductEntity::getProductCode, productCode.trim())
|
||||
.last("LIMIT 1"));
|
||||
if (product == null) {
|
||||
return R.failed("产品不存在");
|
||||
}
|
||||
if (isSatisfied(hotelId, product.getProductCode(), in, out, need, mode)) {
|
||||
return R.failed(anyMode ? "近期已有可订房间,无需订阅" : "所选日期已有房,无需订阅");
|
||||
}
|
||||
SmHotelAvailSubEntity exist = findByKey(userId, hotelId, product.getProductCode(), in, out, need, mode);
|
||||
if (exist != null) {
|
||||
if (SmHotelAvailSubEntity.STATUS_ACTIVE.equals(exist.getStatus())) {
|
||||
return R.ok(exist);
|
||||
}
|
||||
int ver = exist.getNotifyVersion() == null ? 0 : exist.getNotifyVersion();
|
||||
exist.setStatus(SmHotelAvailSubEntity.STATUS_ACTIVE);
|
||||
exist.setWatchMode(mode);
|
||||
exist.setNotifyVersion(ver + 1);
|
||||
exist.setNotifiedTime(null);
|
||||
exist.setHotelName(hotel.getHotelName());
|
||||
exist.setProductName(product.getProductName());
|
||||
availSubMapper.updateById(exist);
|
||||
log.info("|有房通知|重新激活|subId={}|mode={}|hotelId={}|product={}|", exist.getId(), mode, hotelId,
|
||||
product.getProductCode());
|
||||
return R.ok(exist);
|
||||
}
|
||||
SmHotelAvailSubEntity row = new SmHotelAvailSubEntity();
|
||||
row.setUserId(userId);
|
||||
row.setHotelId(hotelId);
|
||||
row.setProductCode(product.getProductCode());
|
||||
row.setCheckIn(in);
|
||||
row.setCheckOut(out);
|
||||
row.setRooms(need);
|
||||
row.setWatchMode(mode);
|
||||
row.setStatus(SmHotelAvailSubEntity.STATUS_ACTIVE);
|
||||
row.setNotifyVersion(0);
|
||||
row.setHotelName(hotel.getHotelName());
|
||||
row.setProductName(product.getProductName());
|
||||
availSubMapper.insert(row);
|
||||
log.info("|有房通知|新建订阅|subId={}|mode={}|userId={}|hotelId={}|product={}|{}~{}|rooms={}|",
|
||||
row.getId(), mode, userId, hotelId, product.getProductCode(), in, out, need);
|
||||
return R.ok(row);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<Boolean> unsubscribe(Long userId, Long id, Long hotelId, String productCode,
|
||||
LocalDate checkIn, LocalDate checkOut, Integer rooms, String watchMode) {
|
||||
SmHotelAvailSubEntity row = null;
|
||||
if (id != null) {
|
||||
row = availSubMapper.selectById(id);
|
||||
}
|
||||
else if (userId != null && hotelId != null && StrUtil.isNotBlank(productCode)) {
|
||||
String mode = normalizeWatchMode(watchMode);
|
||||
LocalDate in;
|
||||
LocalDate out;
|
||||
if (SmHotelAvailSubEntity.WATCH_ANY.equals(mode)) {
|
||||
in = SmHotelAvailSubEntity.ANY_SENTINEL_IN;
|
||||
out = SmHotelAvailSubEntity.ANY_SENTINEL_OUT;
|
||||
}
|
||||
else {
|
||||
in = checkIn;
|
||||
out = checkOut;
|
||||
}
|
||||
if (in != null && out != null) {
|
||||
row = findByKey(userId, hotelId, productCode.trim(), in, out, normalizeRooms(rooms), mode);
|
||||
}
|
||||
}
|
||||
if (row == null || !Objects.equals(row.getUserId(), userId)) {
|
||||
return R.failed("订阅不存在");
|
||||
}
|
||||
if (SmHotelAvailSubEntity.STATUS_CANCELLED.equals(row.getStatus())) {
|
||||
return R.ok(true);
|
||||
}
|
||||
row.setStatus(SmHotelAvailSubEntity.STATUS_CANCELLED);
|
||||
availSubMapper.updateById(row);
|
||||
return R.ok(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<SmHotelAvailSubEntity> pageMine(Page<?> page, Long userId) {
|
||||
return availSubMapper.selectPage(
|
||||
new Page<>(page.getCurrent(), page.getSize()),
|
||||
Wrappers.<SmHotelAvailSubEntity>lambdaQuery()
|
||||
.eq(SmHotelAvailSubEntity::getUserId, userId)
|
||||
.ne(SmHotelAvailSubEntity::getStatus, SmHotelAvailSubEntity.STATUS_CANCELLED)
|
||||
.orderByDesc(SmHotelAvailSubEntity::getUpdateTime)
|
||||
.orderByDesc(SmHotelAvailSubEntity::getId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> status(Long userId, Long hotelId, String productCode,
|
||||
LocalDate checkIn, LocalDate checkOut, Integer rooms) {
|
||||
Map<String, Object> out = new HashMap<>(8);
|
||||
out.put("subscribed", false);
|
||||
out.put("subscriptionId", null);
|
||||
out.put("watchMode", null);
|
||||
if (userId == null || hotelId == null || StrUtil.isBlank(productCode)) {
|
||||
return out;
|
||||
}
|
||||
int need = normalizeRooms(rooms);
|
||||
String code = productCode.trim();
|
||||
// 优先:当前页筛选日期的 RANGE
|
||||
if (checkIn != null && checkOut != null) {
|
||||
SmHotelAvailSubEntity range = availSubMapper.selectOne(Wrappers.<SmHotelAvailSubEntity>lambdaQuery()
|
||||
.eq(SmHotelAvailSubEntity::getUserId, userId)
|
||||
.eq(SmHotelAvailSubEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelAvailSubEntity::getProductCode, code)
|
||||
.eq(SmHotelAvailSubEntity::getCheckIn, checkIn)
|
||||
.eq(SmHotelAvailSubEntity::getCheckOut, checkOut)
|
||||
.eq(SmHotelAvailSubEntity::getRooms, need)
|
||||
.eq(SmHotelAvailSubEntity::getStatus, SmHotelAvailSubEntity.STATUS_ACTIVE)
|
||||
.and(w -> w.eq(SmHotelAvailSubEntity::getWatchMode, SmHotelAvailSubEntity.WATCH_RANGE)
|
||||
.or()
|
||||
.isNull(SmHotelAvailSubEntity::getWatchMode))
|
||||
.last("LIMIT 1"));
|
||||
if (range != null) {
|
||||
out.put("subscribed", true);
|
||||
out.put("subscriptionId", range.getId());
|
||||
out.put("watchMode", SmHotelAvailSubEntity.WATCH_RANGE);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
// 其次:同产品同间数的 ANY(不限日期)
|
||||
SmHotelAvailSubEntity any = availSubMapper.selectOne(Wrappers.<SmHotelAvailSubEntity>lambdaQuery()
|
||||
.eq(SmHotelAvailSubEntity::getUserId, userId)
|
||||
.eq(SmHotelAvailSubEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelAvailSubEntity::getProductCode, code)
|
||||
.eq(SmHotelAvailSubEntity::getRooms, need)
|
||||
.eq(SmHotelAvailSubEntity::getWatchMode, SmHotelAvailSubEntity.WATCH_ANY)
|
||||
.eq(SmHotelAvailSubEntity::getStatus, SmHotelAvailSubEntity.STATUS_ACTIVE)
|
||||
.last("LIMIT 1"));
|
||||
if (any != null) {
|
||||
out.put("subscribed", true);
|
||||
out.put("subscriptionId", any.getId());
|
||||
out.put("watchMode", SmHotelAvailSubEntity.WATCH_ANY);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scheduleCheckAfterHotelChanged(Long hotelId) {
|
||||
if (hotelId == null) {
|
||||
return;
|
||||
}
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
if (tenantId == null) {
|
||||
tenantId = 1L;
|
||||
}
|
||||
Long tid = tenantId;
|
||||
log.info("|有房通知|安排扫描|hotelId={}|tenantId={}|txActive={}|", hotelId, tid,
|
||||
TransactionSynchronizationManager.isSynchronizationActive());
|
||||
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
// 提交后短延迟再扫,确保日历行对查询可见;同酒店合并多次触发
|
||||
scheduleDebounced(hotelId, tid);
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
scheduleDebounced(hotelId, tid);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> scanNow(Long hotelId) {
|
||||
if (hotelId == null) {
|
||||
Map<String, Object> empty = new LinkedHashMap<>();
|
||||
empty.put("hotelId", null);
|
||||
empty.put("activeSubs", 0);
|
||||
empty.put("notified", 0);
|
||||
empty.put("skippedNotBookable", 0);
|
||||
empty.put("feignFailed", 0);
|
||||
return empty;
|
||||
}
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
if (tenantId == null) {
|
||||
tenantId = 1L;
|
||||
}
|
||||
return TenantBroker.applyAs(tenantId, t -> doScanAndNotify(hotelId));
|
||||
}
|
||||
|
||||
private void scheduleDebounced(Long hotelId, Long tenantId) {
|
||||
Long tid = tenantId == null ? 1L : tenantId;
|
||||
ScheduledFuture<?> prev = pendingByHotel.get(hotelId);
|
||||
if (prev != null) {
|
||||
prev.cancel(false);
|
||||
}
|
||||
ScheduledFuture<?> future = scheduler.schedule(() -> {
|
||||
pendingByHotel.remove(hotelId);
|
||||
try {
|
||||
log.info("|有房通知|开始异步扫描|hotelId={}|tenantId={}|", hotelId, tid);
|
||||
TenantBroker.runAs(tid, t -> doScanAndNotify(hotelId));
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|有房通知扫描失败|hotelId={}|{}", hotelId, e.getMessage(), e);
|
||||
}
|
||||
}, DEBOUNCE_MS, TimeUnit.MILLISECONDS);
|
||||
pendingByHotel.put(hotelId, future);
|
||||
}
|
||||
|
||||
private Map<String, Object> doScanAndNotify(Long hotelId) {
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("hotelId", hotelId);
|
||||
List<SmHotelAvailSubEntity> subs = availSubMapper.selectList(Wrappers.<SmHotelAvailSubEntity>lambdaQuery()
|
||||
.eq(SmHotelAvailSubEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelAvailSubEntity::getStatus, SmHotelAvailSubEntity.STATUS_ACTIVE)
|
||||
.last("LIMIT 500"));
|
||||
int active = CollUtil.isEmpty(subs) ? 0 : subs.size();
|
||||
summary.put("activeSubs", active);
|
||||
int notified = 0;
|
||||
int skipped = 0;
|
||||
int feignFailed = 0;
|
||||
if (CollUtil.isEmpty(subs)) {
|
||||
log.info("|有房通知|扫描无ACTIVE订阅|hotelId={}|", hotelId);
|
||||
summary.put("notified", 0);
|
||||
summary.put("skippedNotBookable", 0);
|
||||
summary.put("feignFailed", 0);
|
||||
summary.put("hint", "该酒店没有 ACTIVE 订阅,请确认 App 已订阅成功且表 canteen_hotel_avail_sub.status=ACTIVE");
|
||||
return summary;
|
||||
}
|
||||
List<Map<String, Object>> subBrief = new ArrayList<>();
|
||||
for (SmHotelAvailSubEntity s : subs) {
|
||||
Map<String, Object> b = new LinkedHashMap<>();
|
||||
b.put("id", s.getId());
|
||||
b.put("productCode", s.getProductCode());
|
||||
b.put("watchMode", resolveWatchMode(s));
|
||||
b.put("rooms", s.getRooms());
|
||||
b.put("checkIn", s.getCheckIn() == null ? null : s.getCheckIn().toString());
|
||||
b.put("checkOut", s.getCheckOut() == null ? null : s.getCheckOut().toString());
|
||||
subBrief.add(b);
|
||||
}
|
||||
summary.put("subscriptions", subBrief);
|
||||
log.info("|有房通知|开始扫描|hotelId={}|activeSubs={}|subs={}|", hotelId, active, subBrief);
|
||||
for (SmHotelAvailSubEntity sub : subs) {
|
||||
try {
|
||||
String result = notifyIfReady(sub);
|
||||
switch (result) {
|
||||
case "NOTIFIED" -> notified++;
|
||||
case "NOT_BOOKABLE" -> skipped++;
|
||||
case "FEIGN_FAILED" -> feignFailed++;
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
feignFailed++;
|
||||
log.warn("|有房通知单条失败|subId={}|{}", sub.getId(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
summary.put("notified", notified);
|
||||
summary.put("skippedNotBookable", skipped);
|
||||
summary.put("feignFailed", feignFailed);
|
||||
log.info("|有房通知|扫描结束|hotelId={}|notified={}|skippedNotBookable={}|feignFailed={}|",
|
||||
hotelId, notified, skipped, feignFailed);
|
||||
return summary;
|
||||
}
|
||||
|
||||
private String notifyIfReady(SmHotelAvailSubEntity sub) {
|
||||
if (sub == null || sub.getId() == null) {
|
||||
return "SKIP";
|
||||
}
|
||||
// 兼容:库中 watch_mode 未写入时,用占位日期识别 ANY
|
||||
String mode = resolveWatchMode(sub);
|
||||
int need = normalizeRooms(sub.getRooms());
|
||||
log.info("|有房通知|检查订阅|subId={}|mode={}|dbWatchMode={}|product={}|{}~{}|rooms={}|",
|
||||
sub.getId(), mode, sub.getWatchMode(), sub.getProductCode(),
|
||||
sub.getCheckIn(), sub.getCheckOut(), need);
|
||||
if (!isSatisfied(sub.getHotelId(), sub.getProductCode(), sub.getCheckIn(), sub.getCheckOut(), need, mode)) {
|
||||
if (SmHotelAvailSubEntity.WATCH_ANY.equals(mode)) {
|
||||
LocalDate from = LocalDate.now();
|
||||
LocalDate toEx = from.plusDays(ANY_LOOKAHEAD_DAYS);
|
||||
log.info("|有房通知|ANY暂无可订跳过|subId={}|product={}|window={}~{}|rooms={}|",
|
||||
sub.getId(), sub.getProductCode(), from, toEx, need);
|
||||
}
|
||||
else {
|
||||
List<LocalDate> missing = missingBookableNights(sub.getHotelId(), sub.getProductCode(),
|
||||
sub.getCheckIn(), sub.getCheckOut(), need);
|
||||
log.info("|有房通知|RANGE暂不可订跳过|subId={}|product={}|{}~{}|missingNights={}|",
|
||||
sub.getId(), sub.getProductCode(), sub.getCheckIn(), sub.getCheckOut(),
|
||||
missing.stream().map(LocalDate::toString).collect(Collectors.joining(",")));
|
||||
}
|
||||
return "NOT_BOOKABLE";
|
||||
}
|
||||
int ver = sub.getNotifyVersion() == null ? 0 : sub.getNotifyVersion();
|
||||
String eventId = "HOTEL_ROOM:" + sub.getId() + ":" + ver;
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
if (tenantId == null) {
|
||||
tenantId = sub.getTenantId();
|
||||
}
|
||||
if (tenantId == null) {
|
||||
tenantId = 1L;
|
||||
}
|
||||
String hotelName = StrUtil.blankToDefault(sub.getHotelName(), "酒店");
|
||||
String productName = StrUtil.blankToDefault(sub.getProductName(), sub.getProductCode());
|
||||
HotelRoomAvailableEventDto dto = new HotelRoomAvailableEventDto();
|
||||
dto.setEventId(eventId);
|
||||
dto.setTenantId(tenantId);
|
||||
dto.setRecipientUserId(sub.getUserId());
|
||||
dto.setHotelId(sub.getHotelId());
|
||||
dto.setProductCode(sub.getProductCode());
|
||||
dto.setSourceName(hotelName);
|
||||
dto.setTitle("有房提醒");
|
||||
if (SmHotelAvailSubEntity.WATCH_ANY.equals(mode)) {
|
||||
dto.setContent(hotelName + "「" + productName + "」近期已有空房,可前往查看预订");
|
||||
}
|
||||
else {
|
||||
dto.setContent(hotelName + "「" + productName + "」"
|
||||
+ sub.getCheckIn().format(DAY) + "至" + sub.getCheckOut().format(DAY)
|
||||
+ "已有空房,可前往预订");
|
||||
}
|
||||
Map<String, Object> payload = new HashMap<>(8);
|
||||
payload.put("hotelId", sub.getHotelId());
|
||||
payload.put("productCode", sub.getProductCode());
|
||||
payload.put("watchMode", mode);
|
||||
payload.put("rooms", need);
|
||||
payload.put("subscriptionId", sub.getId());
|
||||
if (SmHotelAvailSubEntity.WATCH_RANGE.equals(mode)) {
|
||||
payload.put("checkIn", sub.getCheckIn().toString());
|
||||
payload.put("checkOut", sub.getCheckOut().toString());
|
||||
}
|
||||
dto.setPayloadJson(JSONUtil.toJsonStr(payload));
|
||||
dto.setDeepLinkPath("/pages/hotel/detail");
|
||||
Map<String, Object> linkParams = new HashMap<>(8);
|
||||
linkParams.put("id", String.valueOf(sub.getHotelId()));
|
||||
linkParams.put("rooms", String.valueOf(need));
|
||||
if (SmHotelAvailSubEntity.WATCH_RANGE.equals(mode) && sub.getCheckIn() != null && sub.getCheckOut() != null) {
|
||||
linkParams.put("checkIn", sub.getCheckIn().toString());
|
||||
linkParams.put("checkOut", sub.getCheckOut().toString());
|
||||
}
|
||||
dto.setDeepLinkParamsJson(JSONUtil.toJsonStr(linkParams));
|
||||
dto.setOccurredTime(LocalDateTime.now());
|
||||
log.info("|有房通知|调用Feign入箱|subId={}|mode={}|eventId={}|userId={}|",
|
||||
sub.getId(), mode, eventId, sub.getUserId());
|
||||
R<Boolean> resp;
|
||||
try {
|
||||
resp = remoteAppMessageService.receiveHotelRoomAvailableEvent(dto);
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|有房通知|Feign异常|subId={}|err={}|", sub.getId(), e.getMessage(), e);
|
||||
return "FEIGN_FAILED";
|
||||
}
|
||||
if (resp == null || !resp.isOk() || !Boolean.TRUE.equals(resp.getData())) {
|
||||
log.warn("|有房通知Feign未成功|subId={}|code={}|msg={}|",
|
||||
sub.getId(), resp == null ? null : resp.getCode(), resp == null ? "null" : resp.getMsg());
|
||||
return "FEIGN_FAILED";
|
||||
}
|
||||
SmHotelAvailSubEntity upd = new SmHotelAvailSubEntity();
|
||||
upd.setId(sub.getId());
|
||||
upd.setStatus(SmHotelAvailSubEntity.STATUS_NOTIFIED);
|
||||
upd.setNotifiedTime(LocalDateTime.now());
|
||||
int updated = availSubMapper.update(upd, Wrappers.<SmHotelAvailSubEntity>lambdaUpdate()
|
||||
.eq(SmHotelAvailSubEntity::getId, sub.getId())
|
||||
.eq(SmHotelAvailSubEntity::getStatus, SmHotelAvailSubEntity.STATUS_ACTIVE));
|
||||
log.info("|有房通知|入箱成功并标记NOTIFIED|subId={}|mode={}|updated={}|", sub.getId(), mode, updated);
|
||||
return "NOTIFIED";
|
||||
}
|
||||
|
||||
private boolean isSatisfied(Long hotelId, String productCode, LocalDate checkIn, LocalDate checkOut,
|
||||
int rooms, String watchMode) {
|
||||
if (SmHotelAvailSubEntity.WATCH_ANY.equals(normalizeWatchMode(watchMode))) {
|
||||
return hasAnyBookableNight(hotelId, productCode, rooms);
|
||||
}
|
||||
// RANGE 但落了 ANY 占位日期时,按 ANY 处理,避免永远扫不到
|
||||
if (isAnySentinel(checkIn, checkOut)) {
|
||||
return hasAnyBookableNight(hotelId, productCode, rooms);
|
||||
}
|
||||
return missingBookableNights(hotelId, productCode, checkIn, checkOut, rooms).isEmpty()
|
||||
&& checkIn != null && checkOut != null && checkOut.isAfter(checkIn);
|
||||
}
|
||||
|
||||
/** 近 ANY_LOOKAHEAD_DAYS 天内是否存在任意一晚可订 */
|
||||
private boolean hasAnyBookableNight(Long hotelId, String productCode, int rooms) {
|
||||
if (hotelId == null || StrUtil.isBlank(productCode)) {
|
||||
return false;
|
||||
}
|
||||
LocalDate from = LocalDate.now();
|
||||
LocalDate toEx = from.plusDays(ANY_LOOKAHEAD_DAYS);
|
||||
List<SmHotelDailyEntity> rows = dailyMapper.selectList(Wrappers.<SmHotelDailyEntity>lambdaQuery()
|
||||
.eq(SmHotelDailyEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelDailyEntity::getProductCode, productCode)
|
||||
.ge(SmHotelDailyEntity::getStayDate, from)
|
||||
.lt(SmHotelDailyEntity::getStayDate, toEx));
|
||||
int rowCnt = rows == null ? 0 : rows.size();
|
||||
int openCnt = 0;
|
||||
if (rows != null) {
|
||||
for (SmHotelDailyEntity row : rows) {
|
||||
if (row == null || 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 >= rooms) {
|
||||
openCnt++;
|
||||
log.info("|有房通知|ANY命中可订日|hotelId={}|product={}|stayDate={}|stock={}|sold={}|need={}|",
|
||||
hotelId, productCode, row.getStayDate(), stock, sold, rooms);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("|有房通知|ANY窗口无命中|hotelId={}|product={}|window={}~{}|dailyRows={}|openEnough={}|need={}|",
|
||||
hotelId, productCode, from, toEx, rowCnt, openCnt, rooms);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isAnySentinel(LocalDate checkIn, LocalDate checkOut) {
|
||||
return SmHotelAvailSubEntity.ANY_SENTINEL_IN.equals(checkIn)
|
||||
&& SmHotelAvailSubEntity.ANY_SENTINEL_OUT.equals(checkOut);
|
||||
}
|
||||
|
||||
private static String resolveWatchMode(SmHotelAvailSubEntity sub) {
|
||||
if (sub == null) {
|
||||
return SmHotelAvailSubEntity.WATCH_RANGE;
|
||||
}
|
||||
if (isAnySentinel(sub.getCheckIn(), sub.getCheckOut())) {
|
||||
return SmHotelAvailSubEntity.WATCH_ANY;
|
||||
}
|
||||
return normalizeWatchMode(sub.getWatchMode());
|
||||
}
|
||||
|
||||
private List<LocalDate> missingBookableNights(Long hotelId, String productCode, LocalDate checkIn,
|
||||
LocalDate checkOut, int rooms) {
|
||||
List<LocalDate> missing = new ArrayList<>();
|
||||
if (hotelId == null || StrUtil.isBlank(productCode) || checkIn == null || checkOut == null
|
||||
|| !checkOut.isAfter(checkIn)) {
|
||||
if (checkIn != null) {
|
||||
missing.add(checkIn);
|
||||
}
|
||||
return missing;
|
||||
}
|
||||
List<LocalDate> nights = new ArrayList<>();
|
||||
for (LocalDate d = checkIn; d.isBefore(checkOut); d = d.plusDays(1)) {
|
||||
nights.add(d);
|
||||
}
|
||||
List<SmHotelDailyEntity> rows = dailyMapper.selectList(Wrappers.<SmHotelDailyEntity>lambdaQuery()
|
||||
.eq(SmHotelDailyEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelDailyEntity::getProductCode, productCode)
|
||||
.ge(SmHotelDailyEntity::getStayDate, checkIn)
|
||||
.lt(SmHotelDailyEntity::getStayDate, checkOut));
|
||||
Set<LocalDate> ok = new HashSet<>();
|
||||
if (rows != null) {
|
||||
for (SmHotelDailyEntity row : rows) {
|
||||
if (row == null || 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 >= rooms) {
|
||||
ok.add(row.getStayDate());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (LocalDate night : nights) {
|
||||
if (!ok.contains(night)) {
|
||||
missing.add(night);
|
||||
}
|
||||
}
|
||||
return missing;
|
||||
}
|
||||
|
||||
private SmHotelAvailSubEntity findByKey(Long userId, Long hotelId, String productCode,
|
||||
LocalDate checkIn, LocalDate checkOut, int rooms, String watchMode) {
|
||||
var q = Wrappers.<SmHotelAvailSubEntity>lambdaQuery()
|
||||
.eq(SmHotelAvailSubEntity::getUserId, userId)
|
||||
.eq(SmHotelAvailSubEntity::getHotelId, hotelId)
|
||||
.eq(SmHotelAvailSubEntity::getProductCode, productCode)
|
||||
.eq(SmHotelAvailSubEntity::getCheckIn, checkIn)
|
||||
.eq(SmHotelAvailSubEntity::getCheckOut, checkOut)
|
||||
.eq(SmHotelAvailSubEntity::getRooms, rooms)
|
||||
.last("LIMIT 1");
|
||||
if (SmHotelAvailSubEntity.WATCH_ANY.equals(watchMode)) {
|
||||
q.eq(SmHotelAvailSubEntity::getWatchMode, SmHotelAvailSubEntity.WATCH_ANY);
|
||||
}
|
||||
else {
|
||||
q.and(w -> w.eq(SmHotelAvailSubEntity::getWatchMode, SmHotelAvailSubEntity.WATCH_RANGE)
|
||||
.or()
|
||||
.isNull(SmHotelAvailSubEntity::getWatchMode));
|
||||
}
|
||||
return availSubMapper.selectOne(q);
|
||||
}
|
||||
|
||||
private static String normalizeWatchMode(String watchMode) {
|
||||
if (SmHotelAvailSubEntity.WATCH_ANY.equalsIgnoreCase(StrUtil.trim(watchMode))) {
|
||||
return SmHotelAvailSubEntity.WATCH_ANY;
|
||||
}
|
||||
return SmHotelAvailSubEntity.WATCH_RANGE;
|
||||
}
|
||||
|
||||
private static int normalizeRooms(Integer rooms) {
|
||||
if (rooms == null || rooms < 1) {
|
||||
return 1;
|
||||
}
|
||||
return Math.min(rooms, 20);
|
||||
}
|
||||
}
|
||||
+63
-3
@@ -22,6 +22,7 @@ import com.bsd.pjcloud.supermarket.integration.downstream.DownstreamPushLogConte
|
||||
import com.bsd.pjcloud.supermarket.integration.shangyou.ShangyouHotelApi;
|
||||
import com.bsd.pjcloud.supermarket.integration.shangyou.ShangyouHotelClient;
|
||||
import com.bsd.pjcloud.supermarket.mapper.*;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelAvailabilityNotifyService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelConfigService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelDeptService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelDownstreamAutoPushService;
|
||||
@@ -91,6 +92,9 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
|
||||
private final SmHotelOpenResourceNotifyService openResourceNotifyService;
|
||||
|
||||
/** 有房通知扫描(日历变更后防抖)。 */
|
||||
private final SmHotelAvailabilityNotifyService availabilityNotifyService;
|
||||
|
||||
private final SmHotelMapper hotelMapper;
|
||||
|
||||
private final SmHotelRoomMapper roomMapper;
|
||||
@@ -106,7 +110,8 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
SmHotelPriceConfigService priceConfigService, SmHotelChannelConfigService channelConfigService,
|
||||
SmHotelChannelDeptService channelDeptService, SmHotelDownstreamPushLogService downstreamPushLogService,
|
||||
@Lazy SmHotelDownstreamAutoPushService downstreamAutoPushService,
|
||||
@Lazy SmHotelOpenResourceNotifyService openResourceNotifyService, SmHotelMapper hotelMapper,
|
||||
@Lazy SmHotelOpenResourceNotifyService openResourceNotifyService,
|
||||
@Lazy SmHotelAvailabilityNotifyService availabilityNotifyService, SmHotelMapper hotelMapper,
|
||||
SmHotelRoomMapper roomMapper, SmHotelProductMapper productMapper, SmHotelDailyMapper dailyMapper,
|
||||
StringRedisTemplate stringRedisTemplate) {
|
||||
this.upstreamClient = upstreamClient;
|
||||
@@ -119,6 +124,7 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
this.downstreamPushLogService = downstreamPushLogService;
|
||||
this.downstreamAutoPushService = downstreamAutoPushService;
|
||||
this.openResourceNotifyService = openResourceNotifyService;
|
||||
this.availabilityNotifyService = availabilityNotifyService;
|
||||
this.hotelMapper = hotelMapper;
|
||||
this.roomMapper = roomMapper;
|
||||
this.productMapper = productMapper;
|
||||
@@ -225,6 +231,7 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
log.info("|酒店拉取上游|结束|hotelCode={}|room={}|product={}|daily={}|tips={}|", hotelCode, roomCount,
|
||||
productCount, dailyCount, upstreamTips);
|
||||
downstreamAutoPushService.scheduleAfterHotelChanged(hotelId);
|
||||
availabilityNotifyService.scheduleCheckAfterHotelChanged(hotelId);
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
@@ -353,6 +360,7 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
result.put("endExclusive", endExclusive.toString());
|
||||
// 回调区间拉价 / 手动滚价后,防抖推下游
|
||||
downstreamAutoPushService.scheduleAfterHotelChanged(hotelId);
|
||||
availabilityNotifyService.scheduleCheckAfterHotelChanged(hotelId);
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
@@ -838,6 +846,37 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
return 20;
|
||||
}
|
||||
|
||||
/** 窗型文案 → 环启 roomWindow:0无窗/1有窗/2部分有窗/99其他 */
|
||||
private static int mapRoomWindowCode(String windowType) {
|
||||
if (StrUtil.isBlank(windowType)) {
|
||||
return 99;
|
||||
}
|
||||
String t = windowType.trim();
|
||||
if (t.contains("部分")) {
|
||||
return 2;
|
||||
}
|
||||
if (t.contains("无窗") || "0".equals(t)) {
|
||||
return 0;
|
||||
}
|
||||
if (t.contains("有窗") || "1".equals(t)) {
|
||||
return 1;
|
||||
}
|
||||
return 99;
|
||||
}
|
||||
|
||||
/** 上游 roomWindow 码 → 本地窗型文案 */
|
||||
private static String mapWindowTypeLabel(Integer roomWindow) {
|
||||
if (roomWindow == null) {
|
||||
return null;
|
||||
}
|
||||
return switch (roomWindow) {
|
||||
case 0 -> "无窗";
|
||||
case 1 -> "有窗";
|
||||
case 2 -> "部分有窗";
|
||||
default -> "其他";
|
||||
};
|
||||
}
|
||||
|
||||
/** 按产品推整窗关房区间;返回推送的 list 段数 */
|
||||
private int pushCloseDailyRanges(String partnerCode, SmHotelEntity hotel, List<SmHotelProductEntity> products,
|
||||
BigDecimal defaultPrice) {
|
||||
@@ -1041,8 +1080,11 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
item.put("extApartmentId", room.getRoomTypeCode());
|
||||
item.put("apartmentName", StrUtil.blankToDefault(room.getRoomTypeName(), room.getRoomTypeCode()));
|
||||
item.put("bedType", room.getBedType());
|
||||
// 环启 roomWindow:0无窗/1有窗/2部分有窗/99其他;本地无字段时按 99
|
||||
item.put("roomWindow", 99);
|
||||
item.put("floorInfo", room.getFloorInfo());
|
||||
item.put("areaSqm", room.getAreaSqm());
|
||||
item.put("windowType", room.getWindowType());
|
||||
// 环启 roomWindow:0无窗/1有窗/2部分有窗/99其他
|
||||
item.put("roomWindow", mapRoomWindowCode(room.getWindowType()));
|
||||
int capacity = room.getCapacity() == null ? 2 : room.getCapacity();
|
||||
item.put("capacity", capacity);
|
||||
item.put("apartmentNumber", roomQtyMap.getOrDefault(room.getRoomTypeCode(), 1));
|
||||
@@ -1680,6 +1722,7 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
SmHotelDailyEntity saved = dailyMapper.selectById(daily.getId());
|
||||
fillDailyRemain(saved);
|
||||
downstreamAutoPushService.scheduleAfterHotelChanged(hotelId);
|
||||
availabilityNotifyService.scheduleCheckAfterHotelChanged(hotelId);
|
||||
return R.ok(saved);
|
||||
}
|
||||
|
||||
@@ -1694,6 +1737,7 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
boolean ok = dailyMapper.deleteById(dailyId) > 0;
|
||||
if (ok) {
|
||||
downstreamAutoPushService.scheduleAfterHotelChanged(hotelId);
|
||||
availabilityNotifyService.scheduleCheckAfterHotelChanged(hotelId);
|
||||
}
|
||||
return R.ok(ok);
|
||||
}
|
||||
@@ -1819,6 +1863,7 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
? String.format("已补齐 %d 天行,跳过已有 %d", toInsert.size(), skippedExist)
|
||||
: String.format("已调整 %d 行,新补齐 %d,配额不足跳过 %d", toUpdate.size(), toInsert.size(), skippedStock);
|
||||
downstreamAutoPushService.scheduleAfterHotelChanged(hotelId);
|
||||
availabilityNotifyService.scheduleCheckAfterHotelChanged(hotelId);
|
||||
return R.ok(data, msg);
|
||||
}
|
||||
|
||||
@@ -1959,6 +2004,21 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
|
||||
room.setBedType(item.getString("bedType"));
|
||||
room.setCapacity(item.getInteger("maxCheckInPeoNum"));
|
||||
room.setRoomNumber(item.getInteger("roomNum"));
|
||||
String floor = StrUtil.blankToDefault(item.getString("floorInfo"), item.getString("floor"));
|
||||
room.setFloorInfo(StrUtil.blankToDefault(floor, null));
|
||||
BigDecimal area = item.getBigDecimal("areaSqm");
|
||||
if (area == null) {
|
||||
area = item.getBigDecimal("area");
|
||||
}
|
||||
if (area == null) {
|
||||
area = item.getBigDecimal("roomArea");
|
||||
}
|
||||
room.setAreaSqm(area);
|
||||
String windowType = item.getString("windowType");
|
||||
if (StrUtil.isBlank(windowType)) {
|
||||
windowType = mapWindowTypeLabel(item.getInteger("roomWindow"));
|
||||
}
|
||||
room.setWindowType(StrUtil.blankToDefault(windowType, null));
|
||||
room.setRawJson(item.toJSONString());
|
||||
room.setUpdateTime(now);
|
||||
if (exist == null) {
|
||||
|
||||
+5
@@ -19,6 +19,7 @@ import com.bsd.pjcloud.supermarket.integration.hotel.upstream.HotelUpstreamOrder
|
||||
import com.bsd.pjcloud.supermarket.integration.hotel.upstream.HotelUpstreamOrderResult;
|
||||
import com.bsd.pjcloud.supermarket.integration.shangyou.ShangyouBookStatus;
|
||||
import com.bsd.pjcloud.supermarket.mapper.*;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelAvailabilityNotifyService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelConfigService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelChannelDeptService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelOrderService;
|
||||
@@ -112,6 +113,8 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
|
||||
private final RemoteDeptService remoteDeptService;
|
||||
|
||||
private final SmHotelAvailabilityNotifyService availabilityNotifyService;
|
||||
|
||||
@Override
|
||||
public Page<SmHotelOrderEntity> pageOrders(Page<SmHotelOrderEntity> page, SmHotelOrderEntity query) {
|
||||
Page<SmHotelOrderEntity> result = baseMapper.selectPage(page, Wrappers.<SmHotelOrderEntity>lambdaQuery()
|
||||
@@ -1928,6 +1931,8 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
|
||||
}
|
||||
}
|
||||
order.setInventoryStatus(SmHotelInventoryStatus.ROLLED_BACK);
|
||||
// 回补后可能重新可订,触发有房通知扫描
|
||||
availabilityNotifyService.scheduleCheckAfterHotelChanged(order.getHotelId());
|
||||
}
|
||||
|
||||
private BigDecimal sumLocalSellAmount(Long productId, LocalDate checkIn, LocalDate checkOut, int rooms) {
|
||||
|
||||
+2
-1
@@ -5,13 +5,14 @@
|
||||
<insert id="insertBatch">
|
||||
INSERT INTO canteen_hotel_room (
|
||||
id, tenant_id, hotel_id, group_code, hotel_code, room_type_code, room_type_name,
|
||||
bed_type, capacity, room_number, raw_json,
|
||||
bed_type, capacity, room_number, floor_info, area_sqm, window_type, raw_json,
|
||||
create_by, create_time, update_by, update_time, del_flag
|
||||
) VALUES
|
||||
<foreach collection="list" item="e" separator=",">
|
||||
(
|
||||
#{e.id}, #{e.tenantId}, #{e.hotelId}, #{e.groupCode}, #{e.hotelCode},
|
||||
#{e.roomTypeCode}, #{e.roomTypeName}, #{e.bedType}, #{e.capacity}, #{e.roomNumber},
|
||||
#{e.floorInfo}, #{e.areaSqm}, #{e.windowType},
|
||||
#{e.rawJson}, #{e.createBy}, #{e.createTime}, #{e.updateBy}, #{e.updateTime}, #{e.delFlag}
|
||||
)
|
||||
</foreach>
|
||||
|
||||
+11
@@ -3,6 +3,7 @@ package com.bsd.pjcloud.thirdparty.api.feign;
|
||||
import com.bsd.pjcloud.common.core.constant.ServiceNameConstants;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.feign.annotation.NoToken;
|
||||
import com.bsd.pjcloud.thirdparty.dto.appmessage.HotelRoomAvailableEventDto;
|
||||
import com.bsd.pjcloud.thirdparty.dto.appmessage.MealVerifiedEventDto;
|
||||
import com.bsd.pjcloud.thirdparty.dto.appmessage.WalletMessageEventDto;
|
||||
import jakarta.validation.Valid;
|
||||
@@ -37,4 +38,14 @@ public interface RemoterThirdAppMessageService {
|
||||
@NoToken
|
||||
@PostMapping("/app-message/inner/meal-verified-event")
|
||||
R<Boolean> receiveMealVerifiedEvent(@Valid @RequestBody MealVerifiedEventDto dto);
|
||||
|
||||
/**
|
||||
* 幂等接收酒店有房通知(旁路,不进商户策略矩阵)。
|
||||
*
|
||||
* @param dto 超市酒店订阅触发的事件快照
|
||||
* @return true表示消息中心已幂等接收
|
||||
*/
|
||||
@NoToken
|
||||
@PostMapping("/app-message/inner/hotel-room-available-event")
|
||||
R<Boolean> receiveHotelRoomAvailableEvent(@Valid @RequestBody HotelRoomAvailableEventDto dto);
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package com.bsd.pjcloud.thirdparty.dto.appmessage;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 超市酒店服务提交的有房通知事件;旁路入箱,不走商户策略与关系订阅。
|
||||
*/
|
||||
@Data
|
||||
public class HotelRoomAvailableEventDto implements Serializable {
|
||||
|
||||
/** 跨服务幂等事件ID,建议 HOTEL_ROOM:{subId}:{notifyVersion}。 */
|
||||
@NotBlank(message = "消息事件ID不能为空")
|
||||
private String eventId;
|
||||
|
||||
/** 事件所属租户ID。 */
|
||||
@NotNull(message = "租户ID不能为空")
|
||||
private Long tenantId;
|
||||
|
||||
/** 订阅用户ID(本人接收)。 */
|
||||
@NotNull(message = "接收用户ID不能为空")
|
||||
private Long recipientUserId;
|
||||
|
||||
/** 酒店ID,写入 payload / 深链。 */
|
||||
@NotNull(message = "酒店ID不能为空")
|
||||
private Long hotelId;
|
||||
|
||||
/** 产品编码快照。 */
|
||||
private String productCode;
|
||||
|
||||
/** 展示标题。 */
|
||||
@NotBlank(message = "标题不能为空")
|
||||
private String title;
|
||||
|
||||
/** 展示正文。 */
|
||||
@NotBlank(message = "正文不能为空")
|
||||
private String content;
|
||||
|
||||
/** 来源名称快照(酒店名),写入 sourceMchName 供列表展示。 */
|
||||
private String sourceName;
|
||||
|
||||
/** 详情 payload JSON;可为空。 */
|
||||
private String payloadJson;
|
||||
|
||||
/** App 深链路径,默认消息详情;可指向酒店详情。 */
|
||||
private String deepLinkPath;
|
||||
|
||||
/** App 深链参数 JSON。 */
|
||||
private String deepLinkParamsJson;
|
||||
|
||||
/** 业务发生时间。 */
|
||||
@NotNull(message = "发生时间不能为空")
|
||||
private LocalDateTime occurredTime;
|
||||
}
|
||||
+11
@@ -37,12 +37,23 @@ public class AppMessageConstants {
|
||||
/** PC人工通知来源服务,区别于自动钱包和用餐事件。 */
|
||||
public static final String SOURCE_THIRD = "THIRD";
|
||||
|
||||
/** 超市酒店业务来源服务,用于有房通知等旁路事件。 */
|
||||
public static final String SOURCE_SUPERMARKET = "SUPERMARKET";
|
||||
|
||||
/** 手动推送的稳定事件编码,只用于消息和投递,不加入自动事件策略或关系订阅矩阵。 */
|
||||
public static final String EVENT_MANUAL_PUSH = "MANUAL_PUSH";
|
||||
|
||||
/**
|
||||
* 酒店有房通知事件编码,旁路入箱,不加入 AppMessageEventEnum / 商户策略矩阵 / 关系订阅。
|
||||
*/
|
||||
public static final String EVENT_HOTEL_ROOM_AVAILABLE = "HOTEL_ROOM_AVAILABLE";
|
||||
|
||||
/** 手动通知在App中的展示分类,跨端协议值为system。 */
|
||||
public static final String CATEGORY_SYSTEM = "system";
|
||||
|
||||
/** 酒店类通知在App中的展示分类。 */
|
||||
public static final String CATEGORY_HOTEL = "hotel";
|
||||
|
||||
/** 新建后等待供应商投递的状态。 */
|
||||
public static final String DELIVERY_PENDING = "PENDING";
|
||||
|
||||
|
||||
+17
@@ -9,10 +9,12 @@ import com.bsd.pjcloud.common.security.util.SecurityUtils;
|
||||
import com.bsd.pjcloud.thirdparty.dto.appmessage.AppRelationSubscriptionBatchSaveDto;
|
||||
import com.bsd.pjcloud.thirdparty.dto.appmessage.AppRelationSubscriptionSaveDto;
|
||||
import com.bsd.pjcloud.thirdparty.dto.appmessage.AppUserPushSettingSaveDto;
|
||||
import com.bsd.pjcloud.thirdparty.dto.appmessage.HotelRoomAvailableEventDto;
|
||||
import com.bsd.pjcloud.thirdparty.dto.appmessage.MealVerifiedEventDto;
|
||||
import com.bsd.pjcloud.thirdparty.dto.appmessage.WalletMessageEventDto;
|
||||
import com.bsd.pjcloud.thirdparty.service.appmessage.ThirdAppMessageService;
|
||||
import com.bsd.pjcloud.thirdparty.service.appmessage.ThirdAppMessageSettingService;
|
||||
import com.bsd.pjcloud.thirdparty.service.appmessage.ThirdHotelRoomNotifyService;
|
||||
import com.bsd.pjcloud.thirdparty.vo.appmessage.AppMessageEventOptionVo;
|
||||
import com.bsd.pjcloud.thirdparty.vo.appmessage.AppMessageVo;
|
||||
import com.bsd.pjcloud.thirdparty.vo.appmessage.AppRelationSubscriptionVo;
|
||||
@@ -50,6 +52,9 @@ public class ThirdAppMessageController {
|
||||
/** 用户Push偏好和关系订阅服务。 */
|
||||
private final ThirdAppMessageSettingService settingService;
|
||||
|
||||
/** 酒店有房通知旁路入箱。 */
|
||||
private final ThirdHotelRoomNotifyService hotelRoomNotifyService;
|
||||
|
||||
/**
|
||||
* 内部幂等接收Wallet Outbox聚合事件。
|
||||
*
|
||||
@@ -74,6 +79,18 @@ public class ThirdAppMessageController {
|
||||
return R.ok(TenantBroker.applyAs(dto.getTenantId(), tenantId -> messageService.receiveMealVerifiedEvent(dto)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部幂等接收酒店有房通知(不进入商户策略/关系订阅矩阵)。
|
||||
*
|
||||
* @param dto 超市酒店事件快照
|
||||
* @return true表示消息中心已完整接收
|
||||
*/
|
||||
@Inner
|
||||
@PostMapping("/inner/hotel-room-available-event")
|
||||
public R<Boolean> receiveHotelRoomAvailableEvent(@Valid @RequestBody HotelRoomAvailableEventDto dto) {
|
||||
return R.ok(TenantBroker.applyAs(dto.getTenantId(), tenantId -> hotelRoomNotifyService.receive(dto)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询当前用户全部或未读消息。
|
||||
*
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.bsd.pjcloud.thirdparty.service.appmessage;
|
||||
|
||||
import com.bsd.pjcloud.thirdparty.dto.appmessage.HotelRoomAvailableEventDto;
|
||||
|
||||
/**
|
||||
* 酒店有房通知旁路入箱:只写本人消息并按用户 Push 总开关创建投递,不读商户策略、不扇出关系消息。
|
||||
*/
|
||||
public interface ThirdHotelRoomNotifyService {
|
||||
|
||||
/**
|
||||
* 幂等接收有房通知事件。
|
||||
*
|
||||
* @param dto 超市模块提交的事件快照
|
||||
* @return true 表示已入箱或此前已幂等完成
|
||||
*/
|
||||
boolean receive(HotelRoomAvailableEventDto dto);
|
||||
}
|
||||
+9
-2
@@ -625,8 +625,15 @@ public class ThirdAppMessageServiceImpl implements ThirdAppMessageService {
|
||||
AppMessageVo vo = new AppMessageVo();
|
||||
BeanUtils.copyProperties(entity, vo);
|
||||
AppMessageEventEnum event = AppMessageEventEnum.fromCode(entity.getEventCode());
|
||||
vo.setCategory(AppMessageConstants.EVENT_MANUAL_PUSH.equals(entity.getEventCode())
|
||||
? AppMessageConstants.CATEGORY_SYSTEM : event == null ? null : event.getCategory());
|
||||
if (AppMessageConstants.EVENT_MANUAL_PUSH.equals(entity.getEventCode())) {
|
||||
vo.setCategory(AppMessageConstants.CATEGORY_SYSTEM);
|
||||
}
|
||||
else if (AppMessageConstants.EVENT_HOTEL_ROOM_AVAILABLE.equals(entity.getEventCode())) {
|
||||
vo.setCategory(AppMessageConstants.CATEGORY_HOTEL);
|
||||
}
|
||||
else {
|
||||
vo.setCategory(event == null ? null : event.getCategory());
|
||||
}
|
||||
vo.setRead(AppMessageConstants.READ.equals(entity.getReadStatus()));
|
||||
vo.setTitle(resolveLocalizedTitle(entity, event));
|
||||
vo.setContent(resolveLocalizedContent(entity, event));
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package com.bsd.pjcloud.thirdparty.service.impl.appmessage;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.bsd.pjcloud.thirdparty.constant.appmessage.AppMessageConstants;
|
||||
import com.bsd.pjcloud.thirdparty.dto.appmessage.HotelRoomAvailableEventDto;
|
||||
import com.bsd.pjcloud.thirdparty.entity.appmessage.ThirdAppMessageEntity;
|
||||
import com.bsd.pjcloud.thirdparty.mapper.appmessage.ThirdAppMessageMapper;
|
||||
import com.bsd.pjcloud.thirdparty.service.appmessage.ThirdAppMessageSettingService;
|
||||
import com.bsd.pjcloud.thirdparty.service.appmessage.ThirdHotelRoomNotifyService;
|
||||
import com.bsd.pjcloud.thirdparty.service.appmessage.ThirdPushDeliveryService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* 酒店有房通知:仿手动推送旁路,避免进入商户策略矩阵。
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ThirdHotelRoomNotifyServiceImpl implements ThirdHotelRoomNotifyService {
|
||||
|
||||
private final ThirdAppMessageMapper messageMapper;
|
||||
|
||||
private final ThirdAppMessageSettingService settingService;
|
||||
|
||||
private final ThirdPushDeliveryService deliveryService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean receive(HotelRoomAvailableEventDto dto) {
|
||||
if (dto == null || StrUtil.isBlank(dto.getEventId()) || dto.getRecipientUserId() == null) {
|
||||
throw new IllegalArgumentException("有房通知参数不完整");
|
||||
}
|
||||
if (ownerMessageExists(dto.getEventId(), dto.getRecipientUserId())) {
|
||||
return true;
|
||||
}
|
||||
String title = trimToMax(dto.getTitle(), AppMessageConstants.MESSAGE_TITLE_MAX_LENGTH);
|
||||
String content = trimToMax(dto.getContent(), AppMessageConstants.MESSAGE_CONTENT_MAX_LENGTH);
|
||||
ThirdAppMessageEntity message = new ThirdAppMessageEntity();
|
||||
message.setTenantId(dto.getTenantId());
|
||||
message.setEventId(dto.getEventId());
|
||||
message.setEventCode(AppMessageConstants.EVENT_HOTEL_ROOM_AVAILABLE);
|
||||
message.setRecipientUserId(dto.getRecipientUserId());
|
||||
message.setSubjectUserId(dto.getRecipientUserId());
|
||||
message.setRecipientType(AppMessageConstants.RECIPIENT_OWNER);
|
||||
message.setSourceService(AppMessageConstants.SOURCE_SUPERMARKET);
|
||||
message.setSourceBizType(AppMessageConstants.EVENT_HOTEL_ROOM_AVAILABLE);
|
||||
message.setSourceBizId(String.valueOf(dto.getHotelId()));
|
||||
message.setSourceMchId(null);
|
||||
message.setSourceMchName(StrUtil.blankToDefault(dto.getSourceName(), null));
|
||||
message.setTitle(title);
|
||||
message.setContent(content);
|
||||
message.setPayloadJson(dto.getPayloadJson());
|
||||
message.setDeepLinkPath(StrUtil.blankToDefault(dto.getDeepLinkPath(), AppMessageConstants.MESSAGE_DETAIL_PATH));
|
||||
message.setDeepLinkParamsJson(dto.getDeepLinkParamsJson());
|
||||
message.setReadStatus(AppMessageConstants.UNREAD);
|
||||
message.setOccurredTime(dto.getOccurredTime());
|
||||
try {
|
||||
if (messageMapper.insert(message) != 1 || message.getId() == null) {
|
||||
throw new IllegalStateException("写入有房通知消息失败");
|
||||
}
|
||||
}
|
||||
catch (DuplicateKeyException conflict) {
|
||||
return true;
|
||||
}
|
||||
String skipReason = settingService.isUserPushEnabled(dto.getRecipientUserId())
|
||||
? null
|
||||
: AppMessageConstants.SKIP_USER_DISABLED;
|
||||
deliveryService.createDelivery(message, skipReason);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean ownerMessageExists(String eventId, Long userId) {
|
||||
Long count = messageMapper.selectCount(Wrappers.<ThirdAppMessageEntity>lambdaQuery()
|
||||
.eq(ThirdAppMessageEntity::getEventId, eventId)
|
||||
.eq(ThirdAppMessageEntity::getRecipientUserId, userId));
|
||||
return count != null && count > 0;
|
||||
}
|
||||
|
||||
private static String trimToMax(String raw, int max) {
|
||||
String text = StrUtil.nullToEmpty(raw).trim();
|
||||
if (text.length() <= max) {
|
||||
return text;
|
||||
}
|
||||
if (max <= 1) {
|
||||
return text.substring(0, max);
|
||||
}
|
||||
return text.substring(0, max - 1) + "…";
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user